Smart Pointers

A smart pointer owns its data and adds behavior on top: Box puts values on the heap, Rc/Arc share ownership, and RefCell allows interior mutability — all still checked by the compiler.

Box<T> — Heap Allocation

Box moves a value to the heap but keeps a single owner on the stack. Use it when a value is too large for the stack, when its size is unknown at compile time (recursive types), or when you must transfer ownership without copying:

Box<T>: a stack pointer owning a heap-allocated value

Figure 1 — the stack holds a pointer; the value lives on the heap and is freed when the Box drops.

fn main() {
    let b = Box::new(5);       // 5 lives on the heap now
    println!("boxed = {b}");   // derefs to 5 automatically

    // recursive type needs a Box: the size of Node would otherwise be infinite
    enum List {
        Cons(i32, Box),
        Nil,
    }
    use List::*;
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    let _ = list;
}

Deref & Drop

Smart pointers behave like references thanks to Deref, and clean up via Drop. You rarely call these traits by name — the compiler and runtime do:

use std::ops::Deref;

struct MyBox(T);

impl MyBox {
    fn new(x: T) -> Self { MyBox(x) }
}

impl Deref for MyBox {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

impl Drop for MyBox {
    fn drop(&mut self) { /* cleanup runs automatically at scope end */ }
}

fn main() {
    let x = MyBox::new(String::from("rust"));
    // Deref lets method calls coerce like a plain &String
    println!("len = {}", x.len());
}

Rc & Arc — Shared Ownership

Ownership normally forbids two owners, but Rc/Arc count references so a value can have several owners at once. Rc is single-threaded; Arc uses atomics and is thread-safe:

Rc<T>: two clones share one heap value with a reference count of 2

Figure 2 — the value lives until the last clone drops and the count reaches zero.

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("shared"));
    let b = Rc::clone(&a);         // increments count to 2, no data copy
    println!("owners of a = {}", Rc::strong_count(&a));
    println!("{a} / {b}");         // both see the same value
}

RefCell — Interior Mutability

RefCell moves the borrow rules from compile time to run time: it lets you mutate through a shared reference, then panics (rather than corrupting memory) if you break the read/write rule at runtime:

use std::cell::RefCell;

fn main() {
    let counter = RefCell::new(0);

    // mutate through a shared (&) reference
    *counter.borrow_mut() += 1;
    *counter.borrow_mut() += 1;

    println!("count = {}", counter.borrow());
}
Practice move: in the lab examples, try the common Rc<RefCell<T>> pattern — shared ownership plus interior mutability — and watch a double borrow_mut() panic at runtime.