Box, Rc and RefCell in Rust: smart pointers explained
When to use Box<T>, Rc<T>, RefCell<T>, Rc<RefCell<T>> and Weak<T> in Rust, with the trade-offs and the runtime borrow panic to avoid.
A smart pointer is a type that acts like a reference but also owns and manages what it points to. You've already used two: String and Vec<T>. The standard library adds three you'll meet constantly.
| Type | Gives you | Cost | Typical use |
|---|---|---|---|
Box<T> |
one owner, value on the heap | one allocation | recursive types, trait objects, large values |
Rc<T> |
several owners (single thread), read-only | allocation + reference count | shared data in trees and graphs, caches |
RefCell<T> |
mutation through a shared reference, checked at run time | a borrow flag | interior mutability |
Rc<RefCell<T>> |
several owners that can all mutate | both | shared mutable state in one thread |
Weak<T> |
a non-owning link to Rc data |
a second count | parent pointers, breaking cycles |
Arc<T>, Mutex<T> |
the thread-safe versions of Rc, RefCell |
atomic operations, locking | sharing across threads |
Box: single ownership on the heap
Box::new(x) moves x to the heap and gives you a pointer that owns it. The box frees the value when it's dropped. You need it for recursive types (a tree node containing nodes: without a box the type would have infinite size, error E0072) and for trait objects like Box<dyn Error> or Vec<Box<dyn Shape>>.
Rc: shared ownership
Rc (reference counted) lets several parts of a program own the same value. Rc::clone(&rc) doesn't copy the data, it increments a counter; the value is freed when the last Rc is dropped. Rc::strong_count(&rc) shows the count. The data inside is read-only:
use std::rc::Rc;
fn main() {
let shared = Rc::new(vec![1, 2]);
shared.push(3);
}
error[E0596]: cannot borrow data in an `Rc` as mutable
Rc is not thread-safe (its counter isn't atomic). The compiler stops you from sending it to another thread; use Arc there.
RefCell: borrow checking at run time
RefCell moves the borrowing rules from compile time to run time. .borrow() gives a shared borrow, .borrow_mut() a mutable one, both through a plain &RefCell<T>. This is interior mutability. The rules still apply: if you call borrow_mut() while another borrow is alive, the program panics with RefCell already borrowed. Keep borrows short (don't hold one across a call that might borrow again), or use try_borrow_mut() to get a Result.
For Copy values, Cell<T> is simpler: get() and set(), no borrows, no panics.
Rc<RefCell>: shared and mutable
Combine them when several owners must change the same data, such as observers sharing a log, or nodes in a graph. Each owner holds an Rc, and calls .borrow_mut() to change the value.
Weak: avoiding cycles
Two Rcs pointing at each other never reach a count of zero, so they leak. Make one direction weak: Rc::downgrade(&rc) gives a Weak, which doesn't keep the value alive; weak.upgrade() returns Option<Rc<T>>. Trees typically use Rc for children and Weak for the parent link.
Advice at work
- Reach for plain ownership and references first.
Rc<RefCell<T>>everywhere is often a sign the data structure should be redesigned (indices into aVec, or an arena). - Write
Rc::clone(&x)rather thanx.clone(), so readers see it's a cheap pointer copy. - In multithreaded code the pair is
Arc<Mutex<T>>(orArc<RwLock<T>>).
Example
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
#[derive(Debug)]
enum Tree {
Leaf(i32),
Node(Box<Tree>, Box<Tree>),
}
fn sum(t: &Tree) -> i32 {
match t {
Tree::Leaf(v) => *v,
Tree::Node(l, r) => sum(l) + sum(r),
}
}
struct Team {
name: String,
members: RefCell<Vec<Rc<Player>>>,
}
struct Player {
name: String,
team: RefCell<Weak<Team>>,
score: Cell<u32>,
}
fn main() {
let tree = Tree::Node(Box::new(Tree::Leaf(1)), Box::new(Tree::Node(Box::new(Tree::Leaf(2)), Box::new(Tree::Leaf(3)))));
println!("tree sum {}", sum(&tree));
let log = Rc::new(RefCell::new(Vec::new()));
let writer_a = Rc::clone(&log);
let writer_b = Rc::clone(&log);
writer_a.borrow_mut().push("a: started");
writer_b.borrow_mut().push("b: started");
println!("{:?}, {} owners", log.borrow(), Rc::strong_count(&log));
let team = Rc::new(Team { name: String::from("Crabs"), members: RefCell::new(Vec::new()) });
let ferris = Rc::new(Player { name: String::from("Ferris"), team: RefCell::new(Weak::new()), score: Cell::new(0) });
*ferris.team.borrow_mut() = Rc::downgrade(&team);
team.members.borrow_mut().push(Rc::clone(&ferris));
ferris.score.set(ferris.score.get() + 10);
let team_name = ferris.team.borrow().upgrade().map(|t| t.name.clone());
println!("{} plays for {:?} with {} points", ferris.name, team_name, ferris.score.get());
println!("team has {} member(s)", team.members.borrow().len());
let first = log.borrow();
println!("second borrow_mut while reading: {}", log.try_borrow_mut().is_err());
println!("first entry {}", first[0]);
}
Output:
tree sum 6
["a: started", "b: started"], 3 owners
Ferris plays for Some("Crabs") with 10 points
team has 1 member(s)
second borrow_mut while reading: true
first entry a: started