Skip to content

Smart pointers

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

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.

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> 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> (“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.
  1. Line 6: The field holds an Rc<Catalog>, not a Catalog: the cart shares the catalog, it does not own it alone.
  2. Line 11: Rc::new moves the catalog to the heap, with a count of 1.
  3. Line 15: Rc::clone(&catalog) copies the pointer and adds 1 to the count. It does not copy the catalog.
  4. Line 22: strong_count is 3 here. When the function returns, the local catalog is 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.

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 for Copy values: get() and set(), no borrows, no panics.

This code compiles, and panics when it runs:

Panics: already borrowed
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.

  • Plain values and references first. Most structs own their fields, and functions borrow. Smart pointers are the exception.
  • Box for recursion and trait objects, not to “put things on the heap” as in Java.
  • Rc or Arc for 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, &mut or RefCell.
  • Weak for every back-reference in an Rc structure.
  • Short borrow(), borrow_mut() and lock() scopes, with no calls to unknown code while they are held.

The task: a report that collects lines, in single-thread code.

  1. WorseArc<Mutex<Vec<String>>> with no threads: a lock and an unwrap() on every access, and &self methods 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)
  2. BetterA plain Vec, and &mut self on 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.

  1. What is the difference between Rc<T> and Arc<T>?

    Show the answer

    Both share ownership with a reference count. Arc uses an atomic counter, so it can be shared across threads. Rc is cheaper, and the compiler does not let it cross a thread boundary.

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

  3. A parent holds Vec<Rc<Child>> and each child holds Rc<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>.

  4. Code has Arc<Mutex<HashMap<..>>>, and there is no thread::spawn or async runtime anywhere. What do you suggest?

    Show the answer

    A plain HashMap, with &mut self on the methods that change it. If shared mutation in one thread is really needed, RefCell is enough.