TL;DR
- Box
: Heap allocation. Single owner. Use for recursive types or large data. - Rc
: Multiple owners (single-thread). Reference counted. Read-only. - RefCell
: Runtime borrow checking. Allows mutation through shared reference. - Arc
: Multiple owners (multi-thread). Thread-safe Rc. - Combine them:
Rc<RefCell<T>>for shared mutable state (single-thread).
Step 1: Box — Heap Allocation
Box is the simplest smart pointer: it puts data on the heap instead of the stack. You need it for three reasons: recursive types (a struct that contains itself has unknown stack size), large data (avoid copying 10MB on the stack), and trait objects (Box<dyn Trait> for dynamic dispatch). Unlike C's malloc/free, Box automatically frees heap memory when it goes out of scope, making heap allocation safe without a garbage collector. It's the foundation that more complex smart pointers build upon.
// Basic usage — put data on heap
let boxed = Box::new(5);
println!("{}", *boxed); // Dereference to access
// Primary use case: Recursive types (unknown size at compile time)
enum List {
Cons(i32, Box<List>), // Box gives it a known size (pointer)
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
// Use case: Trait objects (dynamic dispatch)
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
impl Animal for Dog {
fn speak(&self) -> &str { "Woof" }
}
let animal: Box<dyn Animal> = Box::new(Dog);
println!("{}", animal.speak());
When to Use Box
| Scenario | Why |
|---|---|
| Recursive types (linked list, tree) | Compiler needs known size |
| Large data you don't want on stack | Avoid stack overflow |
Trait objects (Box<dyn Trait>) |
Dynamic dispatch |
| Transferring ownership without copying | Move pointer, not data |
Step 2: Rc — Reference Counting (Single Thread)
Rc (Reference Counted) enables multiple ownership — several parts of your program can own the same heap data, and it's freed when the last owner drops. This exists because Rust's single-ownership model is sometimes too restrictive: graph structures, shared configuration, or DOM-like trees where nodes have multiple parents. Rc tracks how many references exist at runtime and deallocates when the count reaches zero. It's single-threaded only (use Arc for threads) because non-atomic counting is faster.
Multiple owners for the same data. Data is dropped when the last Rc is dropped:
use std::rc::Rc;
// Shared ownership — both `a` and `b` own the data
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // Increment ref count (cheap, just a counter bump)
println!("Count: {}", Rc::strong_count(&a)); // 2
println!("{:?}", a); // [1, 2, 3]
println!("{:?}", b); // [1, 2, 3] — same data
drop(b);
println!("Count: {}", Rc::strong_count(&a)); // 1
// Data freed when count reaches 0
Graph Structures
use std::rc::Rc;
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
// Multiple parents can reference the same child node
let shared_child = Rc::new(Node { value: 3, children: vec![] });
let parent1 = Node {
value: 1,
children: vec![Rc::clone(&shared_child)],
};
let parent2 = Node {
value: 2,
children: vec![Rc::clone(&shared_child)],
};
// shared_child has 3 owners (original + 2 clones)
Step 3: RefCell — Runtime Borrow Checking
RefCell moves Rust's borrow checking from compile time to runtime, enabling "interior mutability" — mutating data behind a shared reference. This exists because the compile-time borrow checker is conservative: some patterns (mock objects in tests, caches inside immutable structs, observer patterns) are safe but can't be proven safe statically. RefCell enforces the same rules (one mutable XOR multiple immutable borrows) but panics at runtime instead of refusing to compile. Use it when you know your access pattern is safe but can't convince the compiler.
Normal Rust checks borrows at compile time. RefCell checks at runtime:
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow (runtime check)
{
let borrowed = data.borrow();
println!("{:?}", borrowed);
}
// Mutable borrow (runtime check)
{
let mut borrowed = data.borrow_mut();
borrowed.push(4);
}
// ❌ Panics at runtime (not compile time!)
// let a = data.borrow();
// let b = data.borrow_mut(); // panic! already borrowed immutably
Why RefCell Exists
// Sometimes the compiler is too conservative
trait Cache {
fn get(&self, key: &str) -> Option<&str>; // &self — can't mutate!
}
struct SmartCache {
data: RefCell<HashMap<String, String>>, // Interior mutability!
}
impl Cache for SmartCache {
fn get(&self, key: &str) -> Option<&str> {
// Can mutate through &self thanks to RefCell
let mut map = self.data.borrow_mut();
if !map.contains_key(key) {
let value = expensive_compute(key);
map.insert(key.to_string(), value);
}
// Note: real implementation needs unsafe or different pattern
// for returning &str from RefCell
todo!()
}
}
Step 4: The Power Combo — Rc<RefCell>
Rc<RefCell<T>> combines shared ownership with interior mutability — multiple owners that can all mutate the inner value. This is Rust's equivalent of a shared mutable variable in garbage-collected languages, but with runtime borrow checking instead of no checking at all. You'll see this pattern in tree/graph structures where multiple nodes need mutable access to shared children, in event systems where handlers mutate shared state, and in any single-threaded scenario requiring the flexibility that ownership rules normally prevent.
Shared ownership + mutation (single-thread):
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
}
fn main() {
let leaf = Rc::new(RefCell::new(Node {
value: 3,
children: vec![],
}));
let branch = Rc::new(RefCell::new(Node {
value: 5,
children: vec![Rc::clone(&leaf)],
}));
// Mutate leaf through shared reference!
leaf.borrow_mut().value = 10;
// Both branch and leaf see the change
println!("{:?}", branch.borrow().children[0].borrow().value); // 10
}
Step 5: Weak References (Prevent Cycles)
Weak references solve the reference counting cycle problem: if A owns B and B owns A (both via Rc), neither will ever be freed because the count never reaches zero — a memory leak. Weak holds a non-owning reference that doesn't increment the count, breaking the cycle. You upgrade Weak to Rc with .upgrade() which returns Option<Rc<T>> (the value might already be dropped). Parent-child trees typically use Rc for parent-to-child and Weak for child-to-parent.
Rc cycles cause memory leaks. Weak breaks cycles:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct TreeNode {
value: i32,
parent: RefCell<Weak<TreeNode>>, // Weak — doesn't prevent drop
children: RefCell<Vec<Rc<TreeNode>>>, // Strong — keeps children alive
}
fn main() {
let root = Rc::new(TreeNode {
value: 0,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(TreeNode {
value: 1,
parent: RefCell::new(Rc::downgrade(&root)), // Weak ref to parent
children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(Rc::clone(&child));
// Access parent (may be dropped)
if let Some(parent) = child.parent.borrow().upgrade() {
println!("Parent value: {}", parent.value);
}
}
Step 6: Comparison Table
Choosing the right smart pointer depends on your ownership and mutability needs. Box for single ownership on heap, Rc/Arc for shared ownership, RefCell/Mutex for interior mutability, and Weak for breaking cycles. Using the wrong one leads to either compile errors (trying to share without Rc) or runtime panics (double borrow with RefCell). This table is your decision guide — identify your constraints (single vs multi-thread, shared vs exclusive, compile-time vs runtime checking) and pick accordingly.
| Smart Pointer | Ownership | Mutability | Thread Safety | Use Case |
|---|---|---|---|---|
Box<T> |
Single | Via ownership | Send + Sync | Heap allocation, recursion |
Rc<T> |
Multiple | Read-only | !Send | Shared read access (single thread) |
Arc<T> |
Multiple | Read-only | Send + Sync | Shared read access (multi-thread) |
RefCell<T> |
Single | Runtime-checked mutation | !Sync | Interior mutability |
Mutex<T> |
Single (with lock) | Lock-guarded mutation | Send + Sync | Shared mutation (multi-thread) |
Cell<T> |
Single | Copy-based mutation | !Sync | Simple interior mutability (Copy types) |
Decision Tree
Need heap allocation?
├── Single owner → Box<T>
└── Multiple owners?
├── Single thread → Rc<T>
│ └── Need mutation? → Rc<RefCell<T>>
└── Multi-thread → Arc<T>
└── Need mutation? → Arc<Mutex<T>> or Arc<RwLock<T>>
Step 7: Cell — Simple Interior Mutability
Cell provides interior mutability for Copy types without the overhead of runtime borrow checking. Unlike RefCell which tracks borrows and can panic, Cell simply copies values in and out — no borrowing at all. This makes it faster and panic-free but limited to types that implement Copy (integers, booleans, small structs). Use it for simple counters, flags, or cached values inside otherwise-immutable structs where RefCell's overhead and panic potential aren't worth it.
For Copy types, Cell is simpler than RefCell:
use std::cell::Cell;
struct Counter {
count: Cell<u32>, // Can mutate through &self
}
impl Counter {
fn new() -> Self {
Counter { count: Cell::new(0) }
}
fn increment(&self) {
// No borrow_mut() needed — just get/set
self.count.set(self.count.get() + 1);
}
fn get(&self) -> u32 {
self.count.get()
}
}
let counter = Counter::new();
counter.increment(); // Works with &self!
counter.increment();
println!("{}", counter.get()); // 2
Interview Questions
-
What's the difference between
Box,Rc, andArc?Box: single owner, heap allocation.Rc: multiple owners, single-thread, reference counted.Arc: multiple owners, multi-thread, atomic reference counted.
-
What's interior mutability?
- The ability to mutate data through a shared (immutable) reference. Achieved via
Cell,RefCell, orMutex. The borrow rules are checked at runtime (RefCell) or enforced by locks (Mutex).
- The ability to mutate data through a shared (immutable) reference. Achieved via
-
When would you use
Rc<RefCell<T>>?- When multiple parts of your code need to both read and mutate the same data, within a single thread. Common in trees, graphs, and observer patterns.
-
How do you prevent reference cycles with
Rc?- Use
Weak<T>for back-references (child → parent). Weak references don't increment the strong count, so when all strong refs are dropped, the data is freed.
- Use