Under The Hood
backend rust Rust 1.75+ (edition 2021)

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.

before → after
Rc across threads (invalid)
let shared = Rc::new(5);
thread::spawn(move || println!("{shared}"));
// E0277: Rc<int> cannot be sent between threads safely
Arc for cross-thread sharing
let 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.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.