Step 12 of 17
Smart pointers
Choose between Box, Rc, Arc, Cell, RefCell, Mutex and Weak for heap allocation, shared ownership and interior mutability.
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:
#[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.
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.
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.
| Type | Enforces the rule… | Thread-safe | Cost |
|---|---|---|---|
Cell<T> | By never handing out references: you get a copy or set a new value | No | None |
RefCell<T> | At runtime: tracks active borrows, panics on a conflict | No | A counter check per borrow |
Mutex<T> | At runtime: one locker at a time, others block | Yes | A lock per access |
RwLock<T> | At runtime: many readers or one writer | Yes | A lock per access |
Atomics (AtomicU64…) | In hardware, for single integers and flags | Yes | An atomic instruction |
Cell is for small Copy values like counters and flags:
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.
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:
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:
- 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. - Does it have more than one owner? One thread:
Rc<T>. Several threads or async tasks:Arc<T>. - Do shared owners need to mutate it? Single thread:
CellforCopyvalues,RefCellotherwise. Several threads: atomics for counters,MutexorRwLockotherwise. - 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.
- Define
struct Account { owner: String, balance: RefCell<i64> }and share accounts withRc<Account>. - Write
fn transfer(from: &Account, to: &Account, amount: i64) -> Result<(), String>that refuses overdrafts. It takes shared references only. - Create two accounts, give a joint holder
Rcclones of both, and perform transfers from each holder. Print the strong counts. - What happens if you call
transfer(&a, &a, 10)with the same account, and your implementation holds bothborrow_mut()guards at once? Fix it so self-transfers work (or are rejected cleanly).
Show solution
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) andArc<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. RefCellpanics on conflicting borrows; keep borrows short, and treat widespreadRefCellas 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.Why does enum List { Cons(i32, List), Nil } fail to compile, and what fixes it?
2.You need several threads to read a large, immutable lookup table loaded at startup. What should you use?
3.What happens if code calls cell.borrow_mut() while a guard from cell.borrow() is still alive?
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.