Smart Pointers
What’s smart about them?
A smart pointer is a struct that wraps a value and gives you pointer-ish
behaviour with bonus features. They look like references (you can * them
and they auto-deref), but they own their contents and they often manage
something extra — a heap allocation, a reference count, a runtime borrow
checker, a Mutex.
You’ve actually been using smart pointers the whole book. Vec<T> and
String are both smart pointers — they own a heap allocation and clean
it up in Drop. The standard library has a few more that you reach for when
the ownership rules from Chapter 3 aren’t quite enough:
| Type | What it gives you |
|---|---|
Box<T> | put a value on the heap, single owner |
Rc<T> | Reference counted, multiple owners, single-thread |
Arc<T> | Atomically reference counted, multiple owners across threads |
RefCell<T> | runtime borrow checking (interior mutability), single-thread |
Mutex<T> | locked access, multiple threads (we’ll meet this in concurrency) |
Cow<T> | Clone on write, share until you mutate |
Let’s take them one at a time.
Box — a value on the heap
Box<T> is the simplest smart pointer. It does exactly one thing: it puts a
T on the heap and gives you a pointer-sized handle to it. When the Box
goes out of scope, the value is dropped and the heap allocation freed.
You reach for Box when:
- The value is big and you don’t want it on the stack.
- You need a recursive data type. The classic example — a linked list, or the calculator expression tree from the pattern matching chapter — would have infinite compile-time size without an indirection somewhere.
- You need a trait object (
Box<dyn Trait>), so the type is hidden behind a pointer.
Trait objects — Box<dyn Trait>
Sometimes you want to hold “any type that implements this trait” without caring which one. That’s a trait object:
The dyn keyword is the giveaway: dynamic dispatch. Each Box<dyn Greet> is
actually a pair of pointers — one to the value, one to the right vtable for
the type. Method calls go through the vtable.
This is your “object-oriented polymorphism” tool when you genuinely need it.
Most of the time generics with trait bounds (the <T: Greet> thing from
Chapter 4) are better because they’re zero cost — but generics require the
compiler to know the type at the call site, and sometimes you don’t.
Rc — shared ownership in one thread
Rule 1 of ownership says: every value has exactly one owner. Rc<T> is
how you bend that rule when you need to.
An Rc is a reference counted pointer. Cloning an Rc doesn’t copy the
inner value — it just bumps a counter. When the last Rc is dropped, the
counter hits zero and the value is freed.
A few important properties of Rc:
Rc::clone(&x)is the idiomatic way to clone an Rc, notx.clone(). Both work, but spelling it out makes “I’m bumping a refcount, not copying data” obvious to the reader.- The contents of an
Rcare immutable. You can read, but you can’t write. (Unless you combine it with aRefCell, which we’re about to do.) Rcis single threaded. The refcount isn’t atomic. Trying to send anRcbetween threads is a compile error.
RefCell — runtime borrow checking
You’ve spent a chapter learning that the compiler enforces “one mutable
borrow OR many immutable borrows” at compile time. RefCell<T> moves
that check to runtime.
Why would you want runtime checks when you could have compile-time? Because sometimes the compiler can’t see your reasoning. The classic case: an internal cache inside an immutable-looking API.
From the outside, count(&self) looks like it doesn’t mutate anything. But
it does — it updates the cache. RefCell is what lets us mutate through a
shared reference. This pattern is called interior mutability and it’s
extremely common.
The cost: if you violate the borrowing rules at runtime (try to mutably borrow
when an immutable borrow is alive, or vice versa), RefCell panics. It’s
on you to make sure your borrow scopes don’t overlap badly.
Rc + RefCell — the combo platter
A common pattern when you need both shared ownership and mutation:
You’ll see Rc<RefCell<T>> in a lot of GUI code, graph structures, and
anything where multiple things need to point at and tweak shared state.
Arc — like Rc but for threads
When you cross thread boundaries, swap Rc for Arc — atomically
reference counted. Same API, slightly more expensive bookkeeping
(fetch_add is more expensive than n += 1). Combine it with Mutex or
RwLock to get the multi-threaded equivalent of Rc<RefCell<T>>.
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new(vec![1, 2, 3]));let shared2 = Arc::clone(&shared);// ...spawn a thread, pass shared2 to it...shared.lock().unwrap().push(4);We’ll see this in action in the concurrency chapter.
Cow — borrow until you have to copy
Cow<'a, T> (“clone on write”) is a little gem you’ll meet in real
code. It’s an enum:
enum Cow<'a, T> where T: ToOwned + ?Sized { Borrowed(&'a T), Owned(T::Owned),}It lets a function say “I’ll usually give you a reference, but if I had to
modify the data, here’s an owned copy.” It’s the answer to “should this take
&str or String?” when sometimes you need to allocate.
The function avoids an allocation in the common case, but still has the option to return an owned value when it needs to.
When to reach for which
A cheat sheet:
- One owner, on the heap:
Box<T>. - Many owners, immutable, one thread:
Rc<T>. - Many owners, mutable, one thread:
Rc<RefCell<T>>. - Many owners, across threads, mutable:
Arc<Mutex<T>>. - Trait object:
Box<dyn Trait>or&dyn Trait. - Maybe-borrow-maybe-own return:
Cow<'a, T>.
90% of Rust code uses none of these — just plain ownership and references. But when you hit a wall with the basic ownership model, one of these is usually the answer.
Onward: concurrency, where these tools really earn their keep.