Smart pointers and interior mutability
Box, Rc, RefCell, Arc and Mutex, Cow, reference cycles and Weak, and how to choose the ownership pattern for a shared data model.
Box, Rc and RefCell
use std::cell::RefCell;
use std::rc::Rc;
// Box: one owner, heap allocation, fixed size for recursive types
enum Tree {
Leaf(i32),
Node(Box<Tree>, Box<Tree>),
}
// Rc: several owners, single-threaded reference counting
#[derive(Debug)]
struct Node {
label: String,
children: RefCell<Vec<Rc<Node>>>, // interior mutability
}
fn main() {
let leaf = Rc::new(Node { label: "leaf".into(), children: RefCell::new(vec![]) });
let root = Rc::new(Node { label: "root".into(), children: RefCell::new(vec![]) });
root.children.borrow_mut().push(Rc::clone(&leaf)); // mutate through &root
println!("{:?} strong={}", root, Rc::strong_count(&root));
// borrow() panics if a mutable borrow is already live
{
let _guard = leaf.children.borrow();
// leaf.children.borrow_mut(); // would panic at run time
}
let boxed = Box::new(42);
println!("{} {}", boxed, (*boxed) + 1);
let t = Tree::Node(Box::new(Tree::Leaf(1)), Box::new(Tree::Leaf(2)));
match t {
Tree::Node(a, _b) => println!("{:?}", matches!(*a, Tree::Leaf(_))),
Tree::Leaf(_) => {}
}
}Box<T>is the cheapest smart pointer: one indirection, one owner, no counters. Use it for recursive types, for large values you want to move cheaply, and for trait objects.Rc<T>shares ownership by counting references on the heap and dropping the value at zero. It is not thread-safe, so it cannot cross a thread boundary — the compiler enforces that.RefCell<T>moves the borrow rules from compile time to run time. You get flexibility, and you get a panic instead of a compile error when you break the rule.borrow()andborrow_mut()return guards. Holding two mutable guards at once panics, so keep them small and never hold one across a call that might borrow again.- The usual combination is
Rc<RefCell<T>>for a mutable shared structure in one thread, andArc<Mutex<T>>for the same across threads.
Arc, Mutex and Cow
use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// lock returns a guard; the guard releases on drop
let mut n = c.lock().unwrap();
*n += 1;
}
}));
}
for h in handles {
h.join().unwrap();
}
println!("{}", counter.lock().unwrap());
// Cow: borrowed until it must be modified
fn normalise(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input) // no allocation on the common path
}
}
println!("{} {}", normalise("a b"), normalise("ab"));
}| Type | Thread-safe | Use it for |
|---|---|---|
Box<T> | Yes | Recursive types, cheap moves, Box<dyn Trait> |
Rc<T> | No | Shared ownership inside one thread |
Arc<T> | Yes | Shared ownership across threads; the atomic counter is the cost |
RefCell<T> | No | Mutation through a shared reference, checked at run time |
Mutex<T> | Yes | Mutation across threads; the lock is the data, not separate from it |
Cow<'a, T> | Depends on T | A value that is usually borrowed and occasionally modified |
RwLockbeatsMutexwhen reads vastly outnumber writes; otherwise the extra bookkeeping makes it slower.Mutexpoisoning: if a thread panics while holding the lock, laterlock()calls returnErr. Decide whether the invariant is broken (propagate) or recoverable (use the guard anyway), rather than reaching forunwrapreflexively.Cowis the right return type for a function that sometimes allocates and often does not, such as normalisation, escaping or default substitution.- For read-mostly shared configuration, prefer
Arc<T>with no lock at all, orarc_swap, over a mutex that is taken on every read.
Cycles and Weak
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Parent {
name: String,
// children own nothing back: Weak breaks the cycle
children: RefCell<Vec<Rc<Child>>>,
}
#[derive(Debug)]
struct Child {
name: String,
parent: Weak<Parent>, // observing, not owning
}
fn main() {
let parent = Rc::new(Parent { name: "p".into(), children: RefCell::new(vec![]) });
let child = Rc::new(Child { name: "c".into(), parent: Rc::downgrade(&parent) });
parent.children.borrow_mut().push(Rc::clone(&child));
let p = child.parent.upgrade(); // Option<Rc<Parent>>
match p {
Some(p) => println!("child belongs to {}", p.name),
None => println!("parent is gone"),
}
// counts: parent 1 strong (the local binding), child 2 strong
println!(
"parent strong={} weak={}",
Rc::strong_count(&parent),
Rc::weak_count(&parent)
);
}- Two
Rcvalues pointing at each other form a cycle, and the strong count never reaches zero, so the memory is never freed. This is the one leak Rust's ownership model cannot prevent on its own. Weakholds a non-owning reference. It does not keep the value alive, andupgrade()returnsNoneonce the strong count hits zero — the safe way to express a back pointer.- The rule of thumb: a tree owns its children with
Rc, and the child observes its parent withWeak. Sibling links, caches and observers are allWeak. Weakis also the basis of a self-referential structure that must not keep itself alive, such as an internal cache keyed by nodes.- In async code the same problem appears as a task holding a handle to itself; there the fix is to drop the handle explicitly when the task finishes.
⚠️
Prefer restructuring over
RefCell when a borrow panic appears. A run-time borrow panic in production is strictly worse than a compile error, and the usual cause is a design where one owner could hold the data plus explicit method calls instead of shared mutable access.FAQ
Rc or Arc?
Rc unless the value crosses a thread boundary. The atomic increments in Arc are measurably slower, and the compiler will tell you when Rc is not enough because Rc is not Send.Why does my program panic on borrow_mut?
Something else is already borrowing the same
RefCell. Look for a guard held across a function call, or a closure that borrows while calling a method on the same value. Drop the first guard before taking the second.Related
Traits, generics and lifetimes Concurrency and async Rust
Last refreshed 2026-09-18.