Rust smart pointers: Box, Rc, Arc
- Last updated
- Prerequisites:
- Rust ownership and borrowing
- Understanding of trait objects and interior mutability
- rust
- smart-pointers
- box
- rc
- arc
- ownership
Read at your depth
The practical view
Box<T> gives a value a heap allocation with single ownership: let b = Box::new(5); (*b) dereferences it. It is the tool for recursive types (enum List { Cons(i32, Box<List>) }), trait objects (Box<dyn Draw>), and large data you want off the stack. Rc<T> is reference-counted shared ownership within one thread: let a = Rc::new(value); let b = Rc::clone(&a); — the value is dropped when the last Rc drops. Arc<T> is the thread-safe version (atomic refcount): Arc::clone is cheap and lets multiple threads share a value; use it with Mutex/RwLock for shared mutable state across threads. Box is 'one owner, heap', Rc is 'many owners, one thread', Arc is 'many owners, many threads'.
Legacy vs modern
Thread-shared state: Rc across threads (compile error) vs Arc
Rc's non-atomic refcount is not Send, so sharing state across threads fails to compile; Arc's atomic count makes the same design legal.
let shared = Rc::new(5);
thread::spawn(move || println!("{shared}"));
// E0277: Rc<int> cannot be sent between threads safelylet shared = Arc::new(Mutex::new(5));
let handle = thread::spawn({
let shared = Arc::clone(&shared);
move || { *shared.lock().unwrap() += 1; }
});Interview gotchas
Context
The interviewer wants the mechanical reason — the refcount's memory ordering — not just 'Rc is not thread-safe'.
The mechanical answer
Rc stores its reference count in a Cell<usize> (or the older AtomicUsize-with-Relaxed variants are NOT used). Cell operations are plain loads/stores with no atomicity or ordering guarantees. If two threads cloned the same Rc concurrently, both would read the count, both would compute +1, and both would write back — losing an increment and eventually freeing the value while a clone still references it (use-after-free). The Send trait is the compile-time guard that forbids this: Rc<T> is !Send (and !Sync), so the compiler rejects the move. Arc fixes it by using atomic fetch_add/fetch_sub with proper memory ordering (Acquire/Release), making concurrent count mutations well-defined.
Trap
The naive answer is 'Rc is not atomic so it's slower' — the non-atomicity is a correctness property for single-threaded performance, not just a speed choice. Another trap: 'Arc is always better, so use it everywhere' — Arc's atomics add contention and memory-ordering fences; for single-threaded ownership Rc is strictly cheaper. Candidates who name Cell vs AtomicUsize and the lost-update failure mode show real understanding.
Context
This is the classic 'shared ownership has a cost' question — the interviewer wants the refcount-never-reaches-zero mechanism and the Weak solution.
The mechanical answer
A reference cycle (a node holding Rc to its parent while the parent holds Rc to the child) keeps every participant's strong count ≥ 1 forever: dropping one side decrements a count that the other side's reference keeps above zero, so no drop glue ever runs and the allocation is never freed. Rust has no cycle-collecting GC by default, so this is a genuine leak. Weak<T> is the escape hatch: it holds a non-owning reference (weak count only) that does not keep the value alive; upgrade() returns Option<Rc<T>>, None if the value was already dropped. Trees/graphs use Rc for parent→child ownership and Weak for child→parent back-references, breaking the cycle so both directions can eventually drop.
Trap
The trap answer is 'just use Arc and the leak goes away' — Arc has the same cycle problem; atomics only fix cross-thread counting, not cycle counting. Another trap: 'Rc frees memory when the count is zero, so cycles are fine if you break them manually' — manual breaking is fragile; Weak is the sanctioned pattern. Candidates who mention upgrade() returning Option and the weak-count mechanics demonstrate production-grade Rust memory fluency.