Smart Pointers: Box, Rc, and RefCell
Regular references only borrow — smart pointers own, count, and sometimes move borrow checks from compile time to runtime, unlocking shapes types alone can't describe.
Every value you've worked with so far follows Rust's core ownership rule: one owner at a time, and the value is dropped the moment that owner goes out of scope. That single rule is what lets Rust guarantee memory safety without a garbage collector, but it is also, by design, restrictive. It doesn't tell you what to do when a value genuinely needs more than one owner, when its size can't be known at compile time, or when you need to mutate something through what the borrow checker sees as an immutable reference. Smart pointers exist to cover exactly those three gaps, and each one you'll meet in this lesson — Box, Rc, RefCell — relaxes a different rule rather than breaking it.
A smart pointer, unlike a plain reference, is a struct that owns the data it points to and usually implements two traits: `Deref`, so it behaves like a reference when you dereference it with `*`, and `Drop`, so it can run cleanup logic the moment it goes out of scope. There's no compiler magic hiding behind them — you could write your own smart pointer type using the same two traits. That's worth internalizing before you look at the code, because it means Box, Rc, and RefCell aren't exceptions to Rust's ownership model; they're built entirely on top of it.
`Box<T>` is the simplest one: it puts a value on the heap instead of the stack, while still enforcing single ownership and compile-time borrow checking exactly like a stack value would. You reach for it in two situations. First, recursive types — a struct that contains itself, like a node in a linked list or a tree — can't have a size the compiler can compute, because it would recurse forever; wrapping the recursive field in a `Box` gives it a fixed size (a pointer is always the same number of bytes) and breaks the infinite recursion. Second, trait objects: when you need a `Vec` or a field to hold 'anything that implements this trait' rather than one concrete type, you store `Box<dyn Trait>`, because the compiler can't size an unknown concrete type but it can always size a pointer to one.
`Rc<T>` — 'reference counted' — solves the opposite problem: shared ownership in single-threaded code. Calling `.clone()` on an `Rc` doesn't copy the underlying data; it bumps an internal counter and hands back another pointer to the same heap allocation. The value is only dropped once that counter reaches zero, meaning the last owner to go out of scope is the one who actually frees it. The trade-off is that `Rc` only ever gives out immutable access — `Deref` gets you a `&T`, never a `&mut T` — because if two owners could mutate the same data simultaneously, Rust's whole aliasing guarantee (either many readers or one writer, never both) would collapse.
That's exactly the gap `RefCell<T>` fills. It enforces Rust's borrowing rules — no more than one mutable borrow, or any number of immutable borrows, never both at once — but it checks them at runtime instead of compile time. Call `.borrow()` for a read-only view or `.borrow_mut()` for a mutable one; each returns a smart guard, and if you ever hold a `borrow_mut()` while another borrow is still alive, the program compiles fine and then panics the moment it runs. That sounds like a downgrade, and in a sense it is — you're trading a compiler error for a runtime one — but it's the only way to mutate something the compiler has decided, for structural reasons, must be immutable.
Put `Rc` and `RefCell` together — `Rc<RefCell<T>>` — and you get exactly what neither one offers alone: a value with multiple owners, any of which can mutate it. This is the standard pattern for shared, mutable state in single-threaded Rust — think of a graph where several nodes need to update a shared counter, or a UI widget tree where a parent and child both need write access to the same piece of state. It's not free: every mutation now carries a runtime borrow check, and it's entirely possible to write code that compiles perfectly and then panics in production the first time two borrows overlap. Used sparingly, it's a precise tool; used everywhere, it's a sign you're fighting the ownership model instead of designing with it — and it's worth knowing that `Weak<T>`, a non-owning version of `Rc`, exists specifically to break the reference cycles this pattern can accidentally create.
// A classic recursive type: a singly linked list built from an enum.// Without Box, Rust cannot compute the size of List (it would contain itself).enum List {Cons(i32, Box<List>),Nil,}use List::{Cons, Nil};fn sum(list: &List) -> i32 {match list {Cons(value, rest) => value + sum(rest),Nil => 0,}}fn main() {let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));println!("sum = {}", sum(&list));}
A recursive enum only compiles once the recursive branch is boxed — the Box gives the compiler a fixed-size pointer to work with instead of an infinitely nested type.
use std::rc::Rc;fn main() {let owner_a = Rc::new(String::from("shared config"));println!("count after creating owner_a: {}", Rc::strong_count(&owner_a));let owner_b = Rc::clone(&owner_a);println!("count after cloning into owner_b: {}", Rc::strong_count(&owner_a));{let owner_c = Rc::clone(&owner_a);println!("count with owner_c alive: {}", Rc::strong_count(&owner_a));println!("owner_c sees: {owner_c}");}println!("count after owner_c dropped: {}", Rc::strong_count(&owner_a));println!("owner_a and owner_b still valid: {owner_a}, {owner_b}");}
Cloning an Rc never copies the data — it just increments the reference count, so every clone is a cheap, shared view of the same heap allocation.
use std::cell::RefCell;use std::rc::Rc;struct SharedCounter {value: i32,}fn main() {let counter = Rc::new(RefCell::new(SharedCounter { value: 0 }));let handle_a = Rc::clone(&counter);let handle_b = Rc::clone(&counter);handle_a.borrow_mut().value += 5;handle_b.borrow_mut().value += 10;println!("final value: {}", counter.borrow().value);println!("total owners: {}", Rc::strong_count(&counter));}
handle_a and handle_b are separate Rc pointers to the same RefCell, so each borrow_mut() call mutates the one shared SharedCounter — this is the pattern to reach for when several owners must all be able to write.
🧠 Check your understanding
0/1 · 0/1 answered1. You need a `Vec<Node>` where `Node` is a struct containing a field of type `Rc<RefCell<Node>>` pointing to its parent, and multiple children need to mutate their shared parent's data. Why is `Rc<T>` alone not enough here, even though `Rc` supports multiple owners?