Answer from a language model, not from memorized syntax: name the mechanism, show a small example, and explain the production consequence.
Question set
7 detailed answers
01When do you reach for Box, Rc, or Arc? How do they differ?
junior
Short answer: Box<T> is a single owner — a plain heap allocation with zero-cost dereference. Rc<T> is shared ownership via a non-atomic reference count, single-threaded only. Arc<T> is the same shared ownership, but the count is updated atomically, so it can be shared across threads (at the cost of atomic operations).
In depth:
Box<T>— one owner, no counter. Use it when a value must live on the heap: recursive structures, trait objects (Box<dyn Trait>), large values. Dereference is free.Rc<T>— several owners in one thread.clone()increments the strong count,dropdecrements it; data is freed when it hits zero. Hands out only&T— put aRefCellinside for mutation.Arc<T>— likeRc, but the count lives in atomics (fetch_add/fetch_sub). ImplementsSend/Sync, fit for multithreading; wrap inMutex/RwLockto mutate.
| Owners | Threads | Type |
|---|---|---|
| one | — | Box<T> |
| many | one | Rc<T> |
| many | several | Arc<T> |
⚠️ Common mistake: reaching for Arc everywhere "just in case". If there's a single owner, Box or even the stack is enough; Arc's atomic operations aren't free, and in one thread they're pure overhead.
02Why doesn't Rc<T> implement Send/Sync while Arc<T> does (given T: Send + Sync)?
middle
Short answer: Rc increments and decrements its reference count with ordinary non-atomic operations. If two threads cloned or dropped the same Rc at once, the race would be on the counter itself — it could go wrong, causing a double free or a leak. That's why Rc: !Send + !Sync. Arc pays for an atomic counter and is therefore thread-safe.
In depth:
- The danger is in the counter, not the data — the race isn't in
T, it's in thestrong/weakfields ofRcBox. An immutableTdoesn't save you: the ref-counting itself breaks. !Send— you can't move anRcto another thread: itsclone/dropthere would race the original.!Sync— you can't hand&Rcto two threads:clone()takes only&self, yet mutates the counter.Arc— the same operations viafetch_add/fetch_sub, atomically; implementsSend + SyncwhenT: Send + Sync.
use std::rc::Rc;
use std::thread;
let counter = Rc::new(42);
let clone = Rc::clone(&counter);
thread::spawn(move || {
println!("{clone}"); // move the Rc into the thread
});
// error[E0277]: `Rc<i32>` cannot be sent between threads safely
// the trait `Send` is not implemented for `Rc<i32>`
⚠️ Common mistake: thinking it's about mutability or the data T itself. The race is on the reference count; swapping T for something thread-safe changes nothing — you need Arc.
03What is interior mutability? How does Cell differ from RefCell?
middle
Short answer: Interior mutability is the ability to mutate a value through a shared reference &T rather than &mut T. The "one mutable access" rule doesn't disappear — it moves from compile time to controlled runtime. Cell works with Copy types via get/set and never hands out references; RefCell does hand out references and checks the borrow rules at runtime, panicking on violation.
In depth:
- Why — when you need mutation behind
&self: insideRc<T>(which gives only&T), caches, counters, graphs. Cell<T>—get/set/replace; never exposes a&/&mut, so there's nothing to check.Copyonly (or viatake/replace).RefCell<T>—borrow()→Ref,borrow_mut()→RefMut; keeps a runtime borrow count and panics on violating "many&XOR one&mut".- Both are
!Sync— single-threaded. For threads useMutex/RwLock/atomics.
Cell |
RefCell |
Mutex |
|
|---|---|---|---|
| Check | none | runtime | runtime |
| On violation | — | panic! |
blocks |
| Hands out refs | no | yes (Ref) |
yes (guard) |
| Threads | one | one | many |
⚠️ Common mistake: believing interior mutability bypasses the borrow rules. It doesn't cancel them — it just moves the check to runtime, where the cost of a mistake is a panic instead of a compile error.
04When does RefCell panic at runtime? Give a realistic borrow_mut() scenario.
middle
Short answer: RefCell panics when borrow_mut() is called while another Ref or RefMut on the same cell is still alive — the message is already borrowed: BorrowMutError. The classic case is a borrow() guard that lives on to the line where the same cell is borrowed again (inside a function call or on the next loop iteration).
In depth:
- The rule — at any moment, either any number of
Ref(&) or exactly oneRefMut(&mut). Break it and you panic at runtime. - Typical scenario — a borrow guard hasn't left its scope, and the code tries to take a
borrow_mut(). - The fix — narrow the guard's lifetime: wrap it in a
{ … }block, ordrop(guard)before re-borrowing, or don't hold aRefacross a call that touches the same cell.
use std::cell::RefCell;
let cell = RefCell::new(vec![1, 2, 3]);
let first = cell.borrow(); // Ref alive...
cell.borrow_mut().push(4); // panic: already borrowed: BorrowMutError
println!("{}", first[0]); // ...until here
// Fix: release the Ref early
{ let first = cell.borrow(); /* … */ } // Ref dies here
cell.borrow_mut().push(4); // ok
⚠️ Common mistake: holding a Ref/RefMut longer than needed — especially across a method call that reaches into the same cell again. Borrow for as short a time as possible.
05What is Weak<T> for? How does an Rc cycle leak memory?
middle
Short answer: Weak<T> is a non-owning reference: it doesn't raise the strong count and doesn't keep the data alive. Its job is to break cycles. If two Rcs point at each other, their strong counts never fall to zero, drop never runs — and memory leaks (safe Rust allows this: memory safety ≠ leak freedom).
In depth:
- How the cycle forms —
aholds anRctob,bholds anRctoa. Even with no external references, bothstrong_counts stay ≥ 1 → the destructor never runs. Weak— created viaRc::downgrade, tracked in a separate weak count.upgrade()→Option<Rc<T>>:Someif the object is alive,Noneif it's already gone.- Parent/child pattern — owning
Rcgoing down (parent → child), non-owningWeakgoing up (child → parent). No cycle, the tree tears down cleanly.
Rc (strong=1) Rc (strong=1)
┌───────────┐ next ┌───────────┐
│ node a │───────► │ node b │
│ │ ◄───────│ │
└───────────┘ prev └───────────┘
both strong refs → counts never reach 0 → leak
Fix: make prev a Weak (strong won't grow) ─► cycle broken
⚠️ Common mistake: building doubly-linked structures (lists, trees with back-references) on bare Rc. The back edge must be Weak, otherwise you get a guaranteed leak.
06What is Cow<'a, str> and when does clone-on-write actually save allocations?
middle
Short answer: Cow<'a, str> (clone-on-write) is an enum of two variants: Borrowed(&'a str) and Owned(String). It lets you return borrowed data with no allocation and only makes a copy when you try to mutate (to_mut/into_owned). It truly saves when the common path returns the input unchanged.
In depth:
- What it is —
enum Cow<'a, B> { Borrowed(&'a B), Owned(B::Owned) }. It derefs to&B, so it reads like a plain&str. - When it wins — "usually nothing to change": sanitizing/escaping a string that's typically already valid; replacing characters that are usually absent. In the common case — 0 allocations.
to_mut()— on the first mutationBorrowedturns intoOwned(one allocation), and edits proceed in place from there.
use std::borrow::Cow;
fn sanitize(input: &str) -> Cow<'_, str> {
if input.contains('\t') {
Cow::Owned(input.replace('\t', " ")) // rare path: allocates
} else {
Cow::Borrowed(input) // common path: no allocation
}
}
⚠️ Common mistake: "Cow is always faster". The enum tag and branching aren't free; if mutation happens almost every time, Cow only complicates the code without saving allocations — just return a String.
07How does Arc's refcounting work: why can the increment be Ordering::Relaxed while the decrement needs Release/Acquire?
senior
Short answer: On clone we copy an already-existing handle — so the object is guaranteed alive, and the increment's ordering synchronizes with nothing: Relaxed is enough. drop, however, must guarantee that all writes to the data finished before the memory is freed: the decrement uses Release, and before running the destructor the last dropper places an Acquire barrier.
In depth:
- Increment —
Relaxed—Arc::cloneis only possible if you already hold a liveArc. It creates no new visibility requirements on the data, there's nothing to synchronize →fetch_add(1, Relaxed). - Decrement —
Release—fetch_sub(1, Release)publishes all of this thread's prior accesses to the data so they "happen-before" a possible teardown. - The last
drop—Acquire— whoever sees the count go 1→0 must see all the Release decrements from other threads. So afence(Acquire)is placed beforedrop_in_place(in std, theacquire!macro).
// std::sync::Arc — simplified
fn clone(&self) -> Arc<T> {
self.inner().strong.fetch_add(1, Relaxed); // object is already alive
Arc { ptr: self.ptr }
}
fn drop(&mut self) {
if self.inner().strong.fetch_sub(1, Release) != 1 {
return; // not the last one — leave
}
atomic::fence(Acquire); // synchronize with all Releases
unsafe { drop_in_place(&mut self.ptr.data) } // now it's safe to free
}
⚠️ Common mistake: saying "it's an atomic, so make it all SeqCst". Relaxed on the increment is correct and cheaper; and forgetting the Acquire barrier before freeing is a data race — the destructor might not see another thread's last writes.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.