Skip to content

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:

TypeWhat 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.

🦀 main.rs

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.
🦀 main.rs

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:

🦀 main.rs

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.

🦀 main.rs

A few important properties of Rc:

  • Rc::clone(&x) is the idiomatic way to clone an Rc, not x.clone(). Both work, but spelling it out makes “I’m bumping a refcount, not copying data” obvious to the reader.
  • The contents of an Rc are immutable. You can read, but you can’t write. (Unless you combine it with a RefCell, which we’re about to do.)
  • Rc is single threaded. The refcount isn’t atomic. Trying to send an Rc between 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.

🦀 main.rs

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:

🦀 main.rs

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.

🦀 main.rs

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.