The Book·Chapter 15·11 min

Smart pointers

Box, Rc, Arc, RefCell, Mutex, and the RAII patterns that make heap allocation explicit

A smart pointer is a struct that owns a heap allocation and behaves like a pointer through the Deref and Drop traits. The standard library ships a small set, each solving a specific ownership problem. Knowing which to reach for is most of the battle.

Box<T>: heap allocation with single ownership

let n: Box<i32> = Box::new(42);
println!("{}", *n);

Box<T> puts T on the heap. The Box itself is a pointer on the stack. When the Box is dropped, the T is dropped and the heap memory is freed.

The most common reason to reach for Box is to make a recursive type sized:

// Won't compile: infinite size
enum List {
    Cons(i32, List),
    Nil,
}
 
// Works: Box gives a pointer-sized indirection
enum List {
    Cons(i32, Box<List>),
    Nil,
}

The other common use is trait objects: Box<dyn Trait>. More on that below.

Rc<T>: single-threaded shared ownership

use std::rc::Rc;
 
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);
let c = Rc::clone(&a);
 
println!("count: {}", Rc::strong_count(&a)); // 3

Rc<T> is reference-counted. Each clone bumps the count; each drop decrements it. When the count hits zero, the value is dropped.

Rc is single-threaded only. It uses a non-atomic counter, which is fast but not Send.

Cycles leak. If a holds an Rc<B> and b holds an Rc<A>, neither count ever hits zero. Break the cycle with Weak<T>:

use std::rc::{Rc, Weak};
 
struct Node {
    parent: Weak<Node>,
    children: Vec<Rc<Node>>,
}

Weak does not contribute to the count. Upgrade with .upgrade() to get an Option<Rc<T>>.

Arc<T>: thread-safe shared ownership

use std::sync::Arc;
 
let data = Arc::new(vec![1, 2, 3]);
let data2 = Arc::clone(&data);
 
std::thread::spawn(move || {
    println!("{:?}", data2);
}).join().unwrap();

Arc<T> is Rc<T> with atomic reference counting. It is Send and Sync when T is. The atomic op makes it a few cycles slower than Rc, which is essentially never a real cost.

Use Arc whenever you cross a thread boundary. Use Rc only when you have measured that the contention does not exist and you want the speed.

RefCell<T> and Cell<T>: interior mutability

The borrow checker normally requires &mut T for any mutation. RefCell<T> shifts that check from compile time to runtime. You can mutate through a &RefCell<T> by calling .borrow_mut(), which returns a RefMut<T> guard.

use std::cell::RefCell;
 
let v = RefCell::new(vec![1, 2, 3]);
v.borrow_mut().push(4);
println!("{:?}", v.borrow());

If two borrow_muts overlap, the second panics. That is the trade: you get mutation without an exclusive reference, you accept runtime panics if the rules are broken.

Cell<T> is similar but for Copy types. No guards, just .get() and .set(). Faster, less flexible.

The combination Rc<RefCell<T>> is the classic "shared mutable state, single-threaded" pattern. Use it sparingly; if the structure is genuinely tree-shaped, prefer a plain &mut.

Mutex<T> and Arc<Mutex<T>>: thread-safe interior mutability

use std::sync::{Arc, Mutex};
 
let counter = Arc::new(Mutex::new(0u64));
 
let handles: Vec<_> = (0..10).map(|_| {
    let counter = Arc::clone(&counter);
    std::thread::spawn(move || {
        let mut g = counter.lock().unwrap();
        *g += 1;
    })
}).collect();
 
for h in handles { h.join().unwrap(); }
println!("{}", counter.lock().unwrap());

Mutex<T> enforces exclusive access at runtime, just like RefCell<T>, but across threads. Pair it with Arc to share the ownership.

Mutex<T> poisons if a thread panics while holding the lock. .lock() returns a Result; the Err variant gives access to the poisoned data if you want it. In production code, deal with this explicitly rather than .unwrap()-ing.

Premature Arc<Mutex<T>>

The most common smart-pointer mistake in agent-written Rust is reaching for Arc<Mutex<T>> when a simpler primitive would do.

△ Bad
use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0u64));

for _ in 0..1000 {
  let c = Arc::clone(&counter);
  std::thread::spawn(move || {
      let mut g = c.lock().unwrap();
      *g += 1;
  });
}
◇ Good
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

let counter = Arc::new(AtomicU64::new(0));

for _ in 0..1000 {
  let c = Arc::clone(&counter);
  std::thread::spawn(move || {
      c.fetch_add(1, Ordering::Relaxed);
  });
}

Atomics for Copy primitives. Channels (mpsc) for ownership transfer. &mut when you can split. Arc<Mutex<T>> only when you genuinely need shared mutable access to a non-Copy aggregate.

The Deref trait and deref coercion

Deref is what lets *box give you the inner value, and what lets you pass &String to a function expecting &str. The compiler chains Deref impls until the target type matches.

fn takes_str(s: &str) { println!("{}", s); }
 
let s: String = String::from("hello");
takes_str(&s);   // &String -> &str via Deref

Most of the time you do not implement Deref yourself. The places where you should: newtype wrappers that genuinely behave as the inner type, smart pointers, and only when the conversion is "this is the thing, just with extra context."

Deref::deref returns &Self::Target. There is also DerefMut::deref_mut for the mutable path.

The Drop trait — RAII

Drop::drop runs when a value goes out of scope. This is how Rust handles file closing, lock release, allocation freeing, all without a garbage collector.

struct LogGuard { name: String }
 
impl Drop for LogGuard {
    fn drop(&mut self) {
        println!("exiting {}", self.name);
    }
}
 
fn work() {
    let _g = LogGuard { name: "work".into() };
    // ... do stuff ...
} // _g dropped here, message prints

You almost never call .drop() directly. To drop early, use std::mem::drop(val). The Drop trait runs automatically, in reverse order of construction, when the scope ends.

This is the foundation of guard patterns: MutexGuard, RefMut, File, TcpStream. All clean up on drop.