Step 12 of 17

Smart pointers

Choose between Box, Rc, Arc, Cell, RefCell, Mutex and Weak for heap allocation, shared ownership and interior mutability.

18 min 8 sections

Ownership says every value has one owner, and borrowing says you can have one writer or many readers. Most code fits those rules. Some doesn't: a recursive data structure, a graph where several nodes point at the same node, a cache that many parts of a program read and occasionally update. Smart pointers are types that own data and add a capability: heap allocation, shared ownership, or mutation through a shared reference, with the checks moved from compile time to runtime where necessary. This lesson covers Box, Rc and Arc, interior mutability with Cell, RefCell and Mutex, reference cycles and Weak, and how to pick one.

Box: a value on the heap

Box<T> puts a value on the heap and owns it. When the box is dropped, so is the value. It has one owner, like any other value, and adds no runtime overhead beyond the allocation.

You need it in three situations.

Recursive types. A type can't contain itself directly, because its size would be infinite. A box has a fixed size (one pointer), which breaks the cycle:

rust
#[derive(Debug)]
enum Expr {
    Num(f64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

fn eval(e: &Expr) -> f64 {
    match e {
        Expr::Num(n) => *n,
        Expr::Add(a, b) => eval(a) + eval(b),
        Expr::Mul(a, b) => eval(a) * eval(b),
    }
}

fn main() {
    // (2 + 3) * 4
    let e = Expr::Mul(
        Box::new(Expr::Add(Box::new(Expr::Num(2.0)), Box::new(Expr::Num(3.0)))),
        Box::new(Expr::Num(4.0)),
    );
    println!("{}", eval(&e)); // 20
}

Trait objects. Box<dyn Trait> owns a value whose concrete type is only known at runtime, as in the traits lesson. Box<dyn std::error::Error> is the common one.

Large values you want to move cheaply. Moving a Box copies a pointer, not the whole value. Useful for big arrays or structs that get passed around a lot.

Box implements Deref, so you can call the inner value's methods directly, and *b gives you the value.

Rc: shared ownership on one thread

Sometimes a value genuinely has several owners, and none of them is clearly the last to need it. Rc<T> (reference counted) allows that. Cloning an Rc doesn't copy the data; it increments a counter and returns another pointer to the same allocation. When the last Rc is dropped, the count hits zero and the value is dropped.

rust
use std::rc::Rc;

#[derive(Debug)]
struct Config {
    name: String,
}

struct Service {
    config: Rc<Config>,
}

fn main() {
    let config = Rc::new(Config { name: "prod".into() });
    let api = Service { config: Rc::clone(&config) };
    let worker = Service { config: Rc::clone(&config) };

    println!("{}", Rc::strong_count(&config)); // 3
    println!("{} {}", api.config.name, worker.config.name);
    drop(api);
    println!("{}", Rc::strong_count(&config)); // 2
}

Writing Rc::clone(&config) instead of config.clone() is a convention: it signals a cheap pointer copy, not a deep clone.

Rc only gives you shared references to the value. Shared ownership plus mutation would break the aliasing rule, so Rc<T> alone is read-only. Rc is also not thread-safe: its counter isn't atomic, so the compiler refuses to send it to another thread.

Arc: shared ownership across threads

Arc<T> (atomically reference counted) is the thread-safe version. The counter uses atomic operations, which cost a little more than Rc's plain increments, so use Rc on a single thread and Arc when the value crosses threads or async tasks.

rust
use std::sync::Arc;
use std::thread;

fn main() {
    let words = Arc::new(vec!["alpha", "beta", "gamma"]);
    let mut handles = Vec::new();
    for i in 0..3 {
        let words = Arc::clone(&words); // each thread gets its own pointer
        handles.push(thread::spawn(move || words[i].len()));
    }
    let total: usize = handles.into_iter().map(|h| h.join().unwrap()).sum();
    println!("{total}"); // 14
}

Arc<T> is how you share read-only state in servers: configuration, a connection pool, a compiled template set. Like Rc, it only hands out shared references.

Interior mutability

What if shared owners need to change the value? Interior mutability types let you mutate data through a shared reference, &T. They don't break the aliasing rule; they enforce it in a different way.

TypeEnforces the rule…Thread-safeCost
Cell<T>By never handing out references: you get a copy or set a new valueNoNone
RefCell<T>At runtime: tracks active borrows, panics on a conflictNoA counter check per borrow
Mutex<T>At runtime: one locker at a time, others blockYesA lock per access
RwLock<T>At runtime: many readers or one writerYesA lock per access
Atomics (AtomicU64…)In hardware, for single integers and flagsYesAn atomic instruction

Cell is for small Copy values like counters and flags:

rust
use std::cell::Cell;

struct Page {
    views: Cell<u32>,
}

impl Page {
    fn view(&self) {               // &self, not &mut self
        self.views.set(self.views.get() + 1);
    }
}

fn main() {
    let p = Page { views: Cell::new(0) };
    p.view();
    p.view();
    println!("{}", p.views.get()); // 2
}

RefCell lends out real references, checked at runtime. borrow() returns a guard that acts like &T, and borrow_mut() one that acts like &mut T. The borrow lasts until the guard is dropped.

rust
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let log = Rc::new(RefCell::new(Vec::<String>::new()));

    let writer = Rc::clone(&log);
    let record = move |msg: &str| writer.borrow_mut().push(msg.to_string());

    record("started");
    record("finished");
    println!("{:?}", log.borrow()); // ["started", "finished"]

    let _reader = log.borrow();
    // log.borrow_mut(); // would panic: already borrowed
}

Rc<RefCell<T>> is the single-threaded idiom for "shared and mutable". Its multithreaded counterpart is Arc<Mutex<T>>, covered in the concurrency lesson. See interior mutability for a compact summary.

Cycles and Weak

Reference counting can't free a cycle: if A holds an Rc to B and B holds one to A, neither count reaches zero, and both leak. Rust's safety guarantees don't cover leaks (they're memory-safe, just wasteful), so this is your job.

The tool is Weak<T>, a non-owning pointer you get from Rc::downgrade. It doesn't keep the value alive; to use it, call upgrade(), which returns Some(Rc<T>) if the value still exists and None if it's gone. The usual pattern is that owners point down with Rc and children point up with Weak:

rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    name: String,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let root = Rc::new(Node { name: "root".into(), parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
    let leaf = Rc::new(Node { name: "leaf".into(), parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });

    *leaf.parent.borrow_mut() = Rc::downgrade(&root);
    root.children.borrow_mut().push(Rc::clone(&leaf));

    let parent = leaf.parent.borrow().upgrade();
    println!("{:?}", parent.map(|p| p.name.clone())); // Some("root")
    println!("{} {}", Rc::strong_count(&root), Rc::weak_count(&root)); // 1 1
}

Graphs with arbitrary edges are often simpler without pointers at all: store nodes in a Vec and refer to them by index (an arena). That sidesteps Rc, RefCell and cycles, and it's what many compilers and game engines do.

Choosing a smart pointer

Ask these questions in order:

  1. Does the value need to be on the heap at all? Recursive type, trait object or very large value: Box<T>. Otherwise, a plain value.
  2. Does it have more than one owner? One thread: Rc<T>. Several threads or async tasks: Arc<T>.
  3. Do shared owners need to mutate it? Single thread: Cell for Copy values, RefCell otherwise. Several threads: atomics for counters, Mutex or RwLock otherwise.
  4. Could ownership form a cycle? Make the back-edges Weak, or switch to indexes into an arena.

So the common combinations are Box<dyn Trait>, Rc<RefCell<T>> on one thread, and Arc<Mutex<T>> or Arc<RwLock<T>> across threads.

Practice

Build a tiny in-memory "bank" where several account holders share accounts.

  1. Define struct Account { owner: String, balance: RefCell<i64> } and share accounts with Rc<Account>.
  2. Write fn transfer(from: &Account, to: &Account, amount: i64) -> Result<(), String> that refuses overdrafts. It takes shared references only.
  3. Create two accounts, give a joint holder Rc clones of both, and perform transfers from each holder. Print the strong counts.
  4. What happens if you call transfer(&a, &a, 10) with the same account, and your implementation holds both borrow_mut() guards at once? Fix it so self-transfers work (or are rejected cleanly).
Show solution
rust
use std::cell::RefCell;
use std::rc::Rc;

struct Account {
    owner: String,
    balance: RefCell<i64>,
}

fn transfer(from: &Account, to: &Account, amount: i64) -> Result<(), String> {
    if std::ptr::eq(from, to) {
        return Err("can't transfer to the same account".into());
    }
    let mut src = from.balance.borrow_mut();
    if *src < amount {
        return Err(format!("{} has insufficient funds", from.owner));
    }
    let mut dst = to.balance.borrow_mut();
    *src -= amount;
    *dst += amount;
    Ok(())
}

fn main() {
    let a = Rc::new(Account { owner: "ana".into(), balance: RefCell::new(100) });
    let b = Rc::new(Account { owner: "bo".into(), balance: RefCell::new(20) });

    let joint = (Rc::clone(&a), Rc::clone(&b));
    transfer(&a, &b, 30).unwrap();
    transfer(&joint.1, &joint.0, 10).unwrap();
    println!("{:?}", transfer(&b, &a, 1_000));     // Err("bo has insufficient funds")

    println!("{} {}", a.balance.borrow(), b.balance.borrow()); // 80 40
    println!("{} {}", Rc::strong_count(&a), Rc::strong_count(&b)); // 2 2
    println!("{:?}", transfer(&a, &a, 10));         // Err("can't transfer to the same account")
}

Without the ptr::eq check, a self-transfer calls borrow_mut() twice on the same RefCell while the first guard is alive, and the program panics with already borrowed: BorrowMutError. That's RefCell doing its job: the same code with plain &mut references would be a compile error. Checking identity up front turns it into a clean Err.

Key takeaways

  • Box<T> puts one owned value on the heap: for recursive types, trait objects and cheap moves of large values.
  • Rc<T> (one thread) and Arc<T> (many threads) allow shared ownership; cloning bumps a counter and the value drops with the last owner.
  • Shared ownership is read-only. Interior mutability (Cell, RefCell, Mutex, RwLock, atomics) moves the aliasing check to runtime.
  • RefCell panics on conflicting borrows; keep borrows short, and treat widespread RefCell as a design smell.
  • Reference cycles leak; break them with Weak, or replace pointers with indexes into an arena.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Why does enum List { Cons(i32, List), Nil } fail to compile, and what fixes it?

  2. 2.You need several threads to read a large, immutable lookup table loaded at startup. What should you use?

  3. 3.What happens if code calls cell.borrow_mut() while a guard from cell.borrow() is still alive?

  4. 4.A tree's children hold Rc pointers to their parent, and the parent holds Rc pointers to its children. What's wrong?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.