Skip to content
Languages

7 Rust Smart Pointers Interview Questions and Answers

This focused guide turns RecallDeck’s curated Rust Smart Pointers material into 7 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

8 min read7 detailed answersReviewed Aug 24, 2026
What to remember

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

01

When do you reach for Box, Rc, or Arc? How do they differ?

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:

  1. 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.
  2. Rc<T> — several owners in one thread. clone() increments the strong count, drop decrements it; data is freed when it hits zero. Hands out only &T — put a RefCell inside for mutation.
  3. Arc<T> — like Rc, but the count lives in atomics (fetch_add/fetch_sub). Implements Send/Sync, fit for multithreading; wrap in Mutex/RwLock to 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.

02

Why doesn't Rc<T> implement Send/Sync while Arc<T> does (given T: Send + Sync)?

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:

  1. The danger is in the counter, not the data — the race isn't in T, it's in the strong/weak fields of RcBox. An immutable T doesn't save you: the ref-counting itself breaks.
  2. !Send — you can't move an Rc to another thread: its clone/drop there would race the original.
  3. !Sync — you can't hand &Rc to two threads: clone() takes only &self, yet mutates the counter.
  4. Arc — the same operations via fetch_add/fetch_sub, atomically; implements Send + Sync when T: 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.

03

What is interior mutability? How does Cell differ from RefCell?

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:

  1. Why — when you need mutation behind &self: inside Rc<T> (which gives only &T), caches, counters, graphs.
  2. Cell<T>get/set/replace; never exposes a &/&mut, so there's nothing to check. Copy only (or via take/replace).
  3. RefCell<T>borrow()Ref, borrow_mut()RefMut; keeps a runtime borrow count and panics on violating "many & XOR one &mut".
  4. Both are !Sync — single-threaded. For threads use Mutex/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.

04

When does RefCell panic at runtime? Give a realistic borrow_mut() scenario.

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:

  1. The rule — at any moment, either any number of Ref (&) or exactly one RefMut (&mut). Break it and you panic at runtime.
  2. Typical scenario — a borrow guard hasn't left its scope, and the code tries to take a borrow_mut().
  3. The fix — narrow the guard's lifetime: wrap it in a { … } block, or drop(guard) before re-borrowing, or don't hold a Ref across 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.

05

What is Weak<T> for? How does an Rc cycle leak memory?

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:

  1. How the cycle formsa holds an Rc to b, b holds an Rc to a. Even with no external references, both strong_counts stay ≥ 1 → the destructor never runs.
  2. Weak — created via Rc::downgrade, tracked in a separate weak count. upgrade()Option<Rc<T>>: Some if the object is alive, None if it's already gone.
  3. Parent/child pattern — owning Rc going down (parent → child), non-owning Weak going 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.

06

What is Cow<'a, str> and when does clone-on-write actually save allocations?

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:

  1. What it isenum Cow<'a, B> { Borrowed(&'a B), Owned(B::Owned) }. It derefs to &B, so it reads like a plain &str.
  2. 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.
  3. to_mut() — on the first mutation Borrowed turns into Owned (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.

07

How does Arc's refcounting work: why can the increment be Ordering::Relaxed while the decrement needs Release/Acquire?

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:

  1. Increment — RelaxedArc::clone is only possible if you already hold a live Arc. It creates no new visibility requirements on the data, there's nothing to synchronize → fetch_add(1, Relaxed).
  2. Decrement — Releasefetch_sub(1, Release) publishes all of this thread's prior accesses to the data so they "happen-before" a possible teardown.
  3. The last dropAcquire — whoever sees the count go 1→0 must see all the Release decrements from other threads. So a fence(Acquire) is placed before drop_in_place (in std, the acquire! 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.

Start studying

Keep going

Languages103 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers
RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS