Smart pointers
In one sentence
Section titled “In one sentence”Smart pointers are library types that own a value and add one capability, such as heap allocation (Box), shared ownership (Rc, Arc), or mutation through a shared reference (RefCell, Mutex).
The Java you know
Section titled “The Java you know”In Java, every object reference can be shared by any number of owners, can change the object, and can cross threads. The garbage collector frees the object, also when references form a cycle. A tree with parent pointers is ordinary Java:
Java
static final class Category { final String name; Category parent; // a reference up final List<Category> children = new ArrayList<>(); // references down
Category(String name) { this.name = name; }
Category addChild(String childName) { var child = new Category(childName); child.parent = this; // a cycle: the garbage collector handles it children.add(child); return child; }}Rust
pub struct Category { pub name: String, pub parent: RefCell<Weak<Category>>, // does not keep the parent alive pub children: RefCell<Vec<Rc<Category>>>, // owns the children}
pub fn add_child(parent: &Rc<Category>, name: &str) -> Rc<Category> { let child = Rc::new(Category { name: name.to_string(), parent: RefCell::new(Rc::downgrade(parent)), children: RefCell::new(Vec::new()), }); parent.children.borrow_mut().push(Rc::clone(&child)); child}
pub fn parent_name(category: &Category) -> Option<String> { category.parent.borrow().upgrade().map(|p| p.name.clone()) // `None` if the parent is gone}In Rust, a plain value has one owner, and only one &mut can change it at a time (see Ownership and moves and Borrowing). When code needs more, it uses a smart pointer. Each one adds one capability, and the type name says which. The Rust version of the tree needs three of them: Rc, RefCell, and Weak.
The Rust you will see
Section titled “The Rust you will see”What a Java reference “is” in Rust
Section titled “What a Java reference “is” in Rust”A Java reference does several jobs at once. In Rust, each job has its own type. This table is the most useful thing to remember from this page:
| The Java reference is used to… | Rust type |
|---|---|
| Point to an object that one owner has | the value itself: Order (no pointer) |
| Read an object that someone else owns | &Order |
| Change an object that someone else owns | &mut Order |
| Put a value on the heap (recursion, trait objects) | Box<Order> |
| Share ownership in one thread | Rc<Order> |
| Share ownership across threads | Arc<Order> |
| Share and change, in one thread | Rc<RefCell<Order>> |
| Share and change, across threads | Arc<Mutex<Order>> or Arc<RwLock<Order>> |
| Point back without keeping alive (parent, cache) | Weak<Order> |
| Be absent | Option<...> around any of these |
Most code needs only the first three rows. Each row further down has a cost: a heap allocation, a reference count, or a check at run time.
Box<T>: one owner, on the heap
Section titled “Box<T>: one owner, on the heap”Box<T> puts a value on the heap and owns it. It behaves like the value itself: when the box is dropped, the value is dropped. You see it for recursive types, which need a fixed size, and for trait objects (Box<dyn Trait>):
/// A price formula. `Box` gives each sub-formula a fixed-size slot on the heap.pub enum Formula { Cents(u64), Sum(Box<Formula>, Box<Formula>), Percent(Box<Formula>, u64),}
pub fn eval(formula: &Formula) -> u64 { match formula { Formula::Cents(c) => *c, Formula::Sum(a, b) => eval(a) + eval(b), Formula::Percent(f, p) => eval(f) * p / 100, }}Rc<T> and Arc<T>: shared ownership
Section titled “Rc<T> and Arc<T>: shared ownership”Rc<T> (“reference counted”) lets several owners share one value. Rc::clone adds one to a counter; dropping an Rc subtracts one. The value is freed when the count reaches zero. The shared value is read-only.
pub struct Catalog { pub products: Vec<String>,}
pub struct Cart { pub catalog: Rc<Catalog>, // shared, read-only, single thread pub items: Vec<usize>,}
pub fn two_carts() -> (Cart, Cart, usize) { let catalog = Rc::new(Catalog { products: vec!["book".into(), "toy".into()], }); let first = Cart { catalog: Rc::clone(&catalog), // +1 to the count items: vec![0], }; let second = Cart { catalog: Rc::clone(&catalog), items: vec![1], }; (first, second, Rc::strong_count(&catalog)) // 3: `catalog`, `first`, `second`} // `catalog` is dropped here: the count goes to 2. The data is freed at 0.- Line 6: The field holds an
Rc<Catalog>, not aCatalog: the cart shares the catalog, it does not own it alone. - Line 11:
Rc::newmoves the catalog to the heap, with a count of 1. - Line 15:
Rc::clone(&catalog)copies the pointer and adds 1 to the count. It does not copy the catalog. - Line 22:
strong_countis 3 here. When the function returns, the localcatalogis dropped, and the count goes to 2.
Arc<T> (“atomically reference counted”) is the same, with an atomic counter, so it can be shared across threads. Rc cannot cross a thread boundary at all: the compiler rejects it, because Rc is not Send. So you never need to check for an Rc race in review. You only need to ask whether Arc is used where Rc (or a plain reference) is enough.
How to read it
Section titled “How to read it”Interior mutability: RefCell<T> and Cell<T>
Section titled “Interior mutability: RefCell<T> and Cell<T>”The borrow rules say: one &mut, or many &. Sometimes that is too strict, for example for a log that many parts of the code write to through a shared reference. RefCell<T> moves the borrow check from compile time to run time:
pub struct AuditLog { entries: RefCell<Vec<String>>, // can change through `&self` writes: Cell<u32>, // a `Copy` value that can change through `&self`}
impl AuditLog { pub fn new() -> Self { Self { entries: RefCell::new(Vec::new()), writes: Cell::new(0), } }
pub fn record(&self, entry: &str) { self.entries.borrow_mut().push(entry.to_string()); // borrow checked at run time self.writes.set(self.writes.get() + 1); }
pub fn count(&self) -> usize { self.entries.borrow().len() }}borrow()returns a shared borrow,borrow_mut()an exclusive one. The rules are the same as for&and&mut.- The difference: a broken rule is not a compile error. It is a panic at run time.
Cell<T>is forCopyvalues:get()andset(), no borrows, no panics.
This code compiles, and panics when it runs:
pub fn record_while_reading(log: &AuditLog) { let entries = log.entries.borrow(); // a shared borrow is active... for entry in entries.iter() { log.record(&format!("read {entry}")); // ...so `borrow_mut` panics: "already borrowed" }}The test refcell_panics_on_conflicting_borrows proves it. When you see RefCell in a review, look for a borrow() that is still alive when a function that calls borrow_mut() runs. These bugs often hide behind a function call, as in this example.
Mutex<T> and RwLock<T>: interior mutability across threads
Section titled “Mutex<T> and RwLock<T>: interior mutability across threads”Mutex<T> is the thread-safe version of RefCell<T>. Where RefCell panics on a conflict, Mutex makes the second thread wait. Shared across threads, it is almost always in an Arc:
pub fn count_in_threads(batches: Vec<Vec<u64>>) -> u64 { let total = Arc::new(Mutex::new(0_u64)); let mut handles = Vec::new(); for batch in batches { let total = Arc::clone(&total); // each thread gets its own handle handles.push(thread::spawn(move || { let sum: u64 = batch.iter().sum(); *total .lock() .expect("no thread panics while holding the lock") += sum; })); } for handle in handles { handle.join().expect("worker thread panicked"); } *total.lock().expect("all workers are done")}Unlike Java’s synchronized, the Mutex owns the data. There is no way to reach the u64 without lock(), so you cannot forget the lock. The lock is released when the guard that lock() returns is dropped. lock() returns a Result because a thread can panic while it holds the lock; the mutex is then “poisoned”. See Threads and shared state.
Weak<T>: references that do not keep a value alive
Section titled “Weak<T>: references that do not keep a value alive”Reference counting cannot free a cycle: if A holds an Rc to B and B holds an Rc to A, both counts stay above zero forever. That is a memory leak. See S35: Rc cycles (memory leak).
Weak<T> breaks the cycle. It points to an Rc value without adding to the strong count. upgrade() returns Option<Rc<T>>: None if the value was already freed. The rule for trees: children are owned (Rc), parents are Weak. That is the tree at the top of this page.
What good looks like
Section titled “What good looks like”- Plain values and references first. Most structs own their fields, and functions borrow. Smart pointers are the exception.
Boxfor recursion and trait objects, not to “put things on the heap” as in Java.RcorArcfor read-only shared data: configuration, a catalog, a cache that is built once.Arc<Mutex<T>>only when threads share data that changes. In single-thread code,&mutorRefCell.Weakfor every back-reference in anRcstructure.- Short
borrow(),borrow_mut()andlock()scopes, with no calls to unknown code while they are held.
Red flags in AI output
Section titled “Red flags in AI output”Rc<RefCell<T>>everywhere, often with.borrow_mut()calls spread over many functions. A sign of a Java object graph translated directly. See S03:Rc<RefCell<T>>object graph (Java translated directly).Arc<Mutex<T>>in single-thread code. Locks and atomic counts for nothing, and every access needslock().unwrap(). See S04:Arc<Mutex<T>>where no sharing across threads exists.Rcin both directions, parent to child and child to parent. A memory leak. See S35:Rccycles (memory leak).Box<T>fields for no reason:Box<Order>whereOrderworks.Derefon a wrapper to imitate inheritance, as if the wrapper were a smart pointer. See S14:Derefused to imitate inheritance.- A
RefCellborrow held across a call to code that can borrow the same cell.
- S03Severity: Medium
Rc<RefCell<T>>object graph (Java translated directly)A Java object graph copied with shared, mutable, reference-counted cells. - S04Severity: Low
Arc<Mutex<T>>where no sharing across threads existsThread-safe wrappers in code that only one thread uses. - S14Severity: Medium
Derefused to imitate inheritanceA wrapper type that auto-dereferences to a "base class". - S22Severity: Low
Box<dyn Trait>where a generic or enum is sufficientDynamic dispatch and a heap allocation where the set of types is known. - S35Severity: Medium
Rccycles (memory leak)Two values that own each other are never freed.
Better vs worse
Section titled “Better vs worse”The task: a report that collects lines, in single-thread code.
Worse
Arc<Mutex<Vec<String>>>with no threads: a lock and anunwrap()on every access, and&selfmethods that hide a change.pub struct Report {lines: Arc<Mutex<Vec<String>>>,}impl Report {pub fn new() -> Self {Self {lines: Arc::new(Mutex::new(Vec::new())),}}pub fn add(&self, line: &str) {self.lines.lock().unwrap().push(line.to_string());}pub fn text(&self) -> String {self.lines.lock().unwrap().join("\n")}}Run in Playground (opens the Rust Playground in a new tab) BetterA plain
Vec, and&mut selfon the method that changes it.#[derive(Default)]pub struct Report {lines: Vec<String>,}impl Report {pub fn add(&mut self, line: &str) {self.lines.push(line.to_string());}pub fn text(&self) -> String {self.lines.join("\n")}}Run in Playground (opens the Rust Playground in a new tab)
The “worse” version works, and in a single thread the lock is never contended. Its costs are in reading and in design: add(&self) looks like a read-only method, but it changes the report. Every access has an unwrap() that a reader must check. And a reader assumes, from the type, that other threads share the report, and looks for them.
The “better” version says in its signatures which method changes the report. If threads are added later, the compiler says where a lock is needed.
Arc<Mutex<T>> is correct when several threads really change the same data, as in count_in_threads above. Even then, ask if each thread can compute its own result, and the results be combined at the end: that needs no lock.
Check yourself
Section titled “Check yourself”What is the difference between
Rc<T>andArc<T>?Show the answer
Both share ownership with a reference count.
Arcuses an atomic counter, so it can be shared across threads.Rcis cheaper, and the compiler does not let it cross a thread boundary.When does
RefCell::borrow_mut()panic?Show the answer
When another borrow of the same
RefCell(shared or mutable) is still alive. The borrow rules are the same as for&and&mut, but they are checked at run time.A parent holds
Vec<Rc<Child>>and each child holdsRc<Parent>. What is the problem?Show the answer
A reference cycle. The strong counts never reach zero, so neither the parent nor the children are ever freed. The child should hold a
Weak<Parent>.Code has
Arc<Mutex<HashMap<..>>>, and there is nothread::spawnor async runtime anywhere. What do you suggest?Show the answer
A plain
HashMap, with&mut selfon the methods that change it. If shared mutation in one thread is really needed,RefCellis enough.