Rust Smart Pointers
The ownership and borrowing rules cover most situations, but a few real cases don’t fit neatly into “exactly one owner” — a value whose size isn’t known until runtime, data that actually needs more than one owner, or a value you need to mutate through a shared reference. Smart pointers are Rust’s answer to each of those.
Box<T>: putting a value on the heap
Section titled “Box<T>: putting a value on the heap”By default, values live on the stack, and the compiler needs to know their size upfront. Box<T> moves a value to the heap and gives you a fixed-size pointer to it — useful when a type’s size can’t be known at compile time, most commonly with recursive types:
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * to get the value out}A classic example of where you need Box: a type that contains itself, like a linked list node:
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("First value: {value}"), List::Nil => println!("Empty"), }}Output:
First value: 1Without Box, Cons(i32, List) would need infinite space to store — a List containing a List containing a List, forever. Box<List> is a fixed-size pointer instead, so the compiler can work out how much space it needs.
Rc<T>: multiple owners
Section titled “Rc<T>: multiple owners”Ownership says a value has exactly one owner. Most of the time that’s exactly right — but sometimes you really do need multiple parts of your program to share the same data. Rc<T> (“reference counted”) allows that, by keeping track of how many owners currently exist:
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("shared data")); let b = Rc::clone(&a); // not a deep copy — just another owner let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("Owners: {}", Rc::strong_count(&a));}Output:
shared data shared data shared dataOwners: 3Rc::clone doesn’t copy the underlying data — it just increments a counter and hands back another reference to the same value. The data only gets cleaned up once every owner has gone out of scope.
RefCell<T>: mutating through a shared reference
Section titled “RefCell<T>: mutating through a shared reference”Normally, the borrowing rules are checked at compile time: either many read-only references, or one mutable one, never both. RefCell<T> moves that same check to runtime instead, which lets you mutate a value even through what looks like a shared, non-mut reference:
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}Output:
6RefCell is most often paired with Rc — Rc<RefCell<T>> gives you a value with multiple owners that can still be mutated, which plain Rc<T> alone doesn’t allow.
When to actually reach for these
Section titled “When to actually reach for these”Don’t start with smart pointers — start with plain ownership and borrowing, and reach for these only when the compiler is actively telling you it won’t fit: a recursive type (Box), a value that actually needs multiple owners (Rc), or mutation through a shared reference that borrowing rules won’t allow (RefCell).
Box<T>stores a value on the heap with a fixed-size pointer — needed for recursive types.Rc<T>allows multiple owners of the same value in single-threaded code, via reference counting; useArc<T>across threads.RefCell<T>checks borrowing rules at runtime instead of compile time, allowing mutation through a shared reference — but panics if the rules are broken.- Reach for these only when plain ownership and borrowing don’t fit your situation.
Quick check
Section titled “Quick check”Quick check
1. Why does a recursive type like a linked list node need Box<T>?
2. What does Rc::clone(&a) actually do?
3. When does RefCell<T> check the borrowing rules?
4. What does Rc<RefCell<T>> give you that plain Rc<T> alone doesn't?
Score: 0 / 4