Site

Smart Pointers — Box, Rc, RefCell

Tutorial S8.0  •  Rust / Learn / StdLib

S8.0 What This Teaches

Rust's ownership model covers most cases, but some designs genuinely need heap allocation, shared ownership, or interior mutability. Smart pointers provide these capabilities without sacrificing safety. This tutorial covers:

S8.1 Box<T> - Heap Allocation

Box<T> stores a value on the heap and gives you a single owning pointer to it. When the Box is dropped, the heap memory is freed:
let b = Box::new(42);
println!("{b}");    // 42 - Deref coercion lets you use it like a plain i32
println!("{}", *b); // explicit deref
The main practical use is enabling recursive types. Without Box, the compiler cannot determine the size of a type that contains itself:
enum List {
    Cons(i32, Box<List>),  // Box breaks the infinite size cycle
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Box<List> has a known, fixed size (one pointer), so the compiler accepts it.

S8.2 Rc<T> - Shared Ownership

Rc<T> (reference-counted pointer) allows multiple owners of the same heap value on a single thread. Each Rc::clone increments a counter; when the counter reaches zero, the value is freed:
use std::rc::Rc;

let shared = Rc::new(String::from("shared data"));
let c1 = Rc::clone(&shared);   // counter becomes 2; no heap copy
let c2 = Rc::clone(&shared);   // counter becomes 3

println!("count: {}", Rc::strong_count(&shared));  // 3
drop(c1);
println!("count: {}", Rc::strong_count(&shared));  // 2
Rc is read-only shared ownership - every clone points to the same allocation and none can mutate it through Rc alone. Rc is not safe to send across threads; use Arc<T> (atomic reference count) when sharing across threads.

S8.3 RefCell<T> - Interior Mutability

Rust's borrow rules are normally enforced at compile time. RefCell<T> defers those checks to runtime, allowing mutation through a shared reference:
use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);

{
    let mut v = data.borrow_mut();  // runtime check: panics if already borrowed
    v.push(4);
}   // mutable borrow released here

println!("{:?}", data.borrow());   // [1, 2, 3, 4]
The borrow rules still hold - you cannot have a borrow_mut and a borrow active at the same time - but violations become runtime panics instead of compile errors. Use RefCell only when the compiler's static analysis is too conservative and you can reason that the rules are not violated at runtime.

S8.4 Rc<RefCell<T>> - Shared Mutable State

Combine Rc and RefCell when multiple owners need to mutate a shared value:
use std::rc::Rc;
use std::cell::RefCell;

let counter = Rc::new(RefCell::new(0));
let c1 = Rc::clone(&counter);
let c2 = Rc::clone(&counter);

*c1.borrow_mut() += 1;
*c2.borrow_mut() += 1;

println!("{}", counter.borrow());   // 2
For multithreaded shared mutation, use Arc<Mutex<T>> instead (covered in the Threads tutorial).

S8.5 Example - All Together

// SmartPointers - demonstrates Box<T>, Rc<T>, and RefCell<T> in Rust.

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
#[allow(dead_code)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    // --- Box<T>: single-owner heap allocation ---
    println!("--- Box ---");
    let b = Box::new(42);
    println!("boxed: {b}, deref: {}", *b);

    // Box makes recursive types possible by giving the compiler a known size
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    println!("{list:?}");

    // --- Rc<T>: shared ownership on a single thread ---
    println!("--- Rc ---");
    let shared = Rc::new(String::from("shared data"));
    let c1 = Rc::clone(&shared);
    let c2 = Rc::clone(&shared);
    println!("value: {shared}, ref count: {}", Rc::strong_count(&shared));
    drop(c1);
    println!("after drop c1, count: {}", Rc::strong_count(&shared));
    drop(c2);
    println!("after drop c2, count: {}", Rc::strong_count(&shared));

    // --- RefCell<T>: interior mutability, borrow checked at runtime ---
    println!("--- RefCell ---");
    let data = RefCell::new(vec![1, 2, 3]);
    {
        let mut v = data.borrow_mut();
        v.push(4);
    }
    println!("{:?}", data.borrow());

    // --- Rc<RefCell<T>>: shared ownership + interior mutability ---
    println!("--- Rc<RefCell<T>> ---");
    let counter = Rc::new(RefCell::new(0));
    let c1 = Rc::clone(&counter);
    let c2 = Rc::clone(&counter);
    *c1.borrow_mut() += 1;
    *c2.borrow_mut() += 1;
    println!("counter: {}", counter.borrow());
}
Expected output:
--- Box ---
boxed: 42, deref: 42
Cons(1, Cons(2, Nil))
--- Rc ---
value: shared data, ref count: 3
after drop c1, count: 2
after drop c2, count: 1
--- RefCell ---
[1, 2, 3, 4]
--- Rc<RefCell<T>> ---
counter: 2

S8.6 Exercise

Exercise
  • Define a recursive Tree enum using Box: Tree::Leaf(i32) and Tree::Node(Box<Tree>, Box<Tree>). Build a small tree and write a function sum(t: &Tree) -> i32 that sums all leaf values.
  • Create two Rc<String> clones of the same string. Confirm the reference count is 3 (original + 2 clones). Drop one and confirm it drops to 2.
  • Use Rc<RefCell<Vec<i32>>> to share a vec between two "owners" (two separate variables). Append a value through each and print the final vec through the original.

S8.7 Common Mistakes

Using Rc across threads

use std::rc::Rc;
let r = Rc::new(42);
std::thread::spawn(move || println!("{r}"));  // error: Rc cannot be sent across threads
Rc is not Send. Use Arc<T> for cross-thread shared ownership.

Calling borrow_mut twice without releasing

let cell = RefCell::new(0);
let a = cell.borrow_mut();
let b = cell.borrow_mut();  // panics at runtime: already mutably borrowed
The first borrow_mut must be dropped before taking a second one. Structure code so borrows are short-lived within a { } block.

Boxing unnecessarily

let x = Box::new(42_i32);  // rarely useful - i32 is Copy and already small
Box values that are large (to avoid stack overflow), that need a stable address, or that are part of a recursive type. Do not Box as a default.

S8.8 Key Terms

TermMeaning
Box<T>Single-owner heap pointer; frees memory when dropped
Rc<T>Reference-counted shared ownership; single-threaded only
Rc::cloneIncrements the reference count; does not copy the heap data
Rc::strong_countReturns the current reference count
RefCell<T>Enforces borrow rules at runtime instead of compile time
borrow()Returns an immutable Ref<T> guard; panics if mutably borrowed
borrow_mut()Returns a mutable RefMut<T> guard; panics if any borrow is active
interior mutabilityMutating data through a shared reference, enforced at runtime
Arc<T>Atomic reference count; like Rc but safe across threads
Mutex<T>Mutual exclusion lock; pairs with Arc for shared mutable state across threads