Interview · Smart Pointers · Fearless Concurrency

Smart Pointers & Concurrency Q&A

The Rust Book chapters 15 & 16 turned into interview answers — Box, Deref, Drop, Rc, RefCell, Weak, threads, mpsc channels, Mutex, Arc, Send and Sync. Plain-language answers with compilable code examples — read each answer, then say it in your own words.

48 questions 11 topics Rust Book · Ch 15–16

Smart Pointer Basics

What is a smart pointer in Rust?
  • Answer: A smart pointer is a struct that points to data like a normal reference does, but it also owns that data and carries extra behavior. Because it is a struct, it can implement Deref (so *ptr works like a regular reference) and Drop (so cleanup runs automatically when it goes out of scope). References &T only borrow data and have zero overhead; smart pointers usually own the data they point to.
  • Real-life example: A normal reference is like a house key — it just opens the door. A smart pointer is like a hotel key card — it still opens the door, but it also knows your checkout time and deactivates itself when you leave.
How is a smart pointer different from a plain `&T` reference?
  • Answer: Three main differences: a reference only borrows while a smart pointer owns; a reference has no extra metadata while smart pointers can carry things like a reference count; and a smart pointer can run code on cleanup through Drop. You use references for cheap temporary access and smart pointers when ownership semantics or extra capabilities are needed.
Which smart pointers does the standard library provide, and when do you pick each?
  • Answer:
  • Box<T> — one owner, data on the heap. For recursive types, big data you want to move cheaply, and trait objects.
  • Rc<T> — many owners, one thread. For shared read access when you can't say at compile time who finishes last.
  • RefCell<T> — one owner, but borrow rules checked at runtime. For mutating data behind an API that looks immutable.
  • Arc<T> — many owners, many threads. The atomic version of Rc.
  • Mutex<T> / RwLock<T> — safe shared mutation between threads.
  • Weak<T> — a non-owning reference to Rc/Arc data, used to break reference cycles.

`Box<T>` — Data on the Heap

What does `Box<T>` actually do?
  • Answer: It stores the value on the heap instead of the stack. The Box itself is just a small pointer that sits on the stack and owns that heap allocation. When the box goes out of scope, both the pointer and the heap data are freed. Other than the heap allocation there is no runtime cost — no counting, no locking.
  • Example:
let b = Box::new(5);   // 5 lives on the heap; b is a pointer on the stack
println!("b = {}", *b); // b = 5 — deref works like a normal reference
When should you use `Box<T>`? What are the three classic cases?
  • Answer:
  1. The type's size can't be known at compile time — recursive types like linked lists or trees, where Box gives the field a fixed pointer size.
  2. You have a large value and want to transfer ownership without copying the data — only the pointer moves, the heap data stays put.
  3. You want a trait object (Box<dyn Trait>) — you own a value and only care that it implements a trait, not which concrete type it is.
  • Real-life example: Instead of carrying a piano from room to room, you hand over a slip that says "the piano is in storage unit 42." The slip is cheap to move; the piano stays in place.
Why do recursive types fail to compile without `Box`?
  • Answer: Rust must know every type's exact size at compile time. In Cons(i32, List) the List field contains another List, which contains another List — so the size never stops growing and the compiler reports "recursive type has infinite size." Wrapping the field in Box<List> breaks the cycle because a box is always pointer-sized, giving the enum a known, finite size.
  • Example:
enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
When should you NOT use `Box<T>`?
  • Answer: For small values that live fine on the stack — boxing a single i32 adds a heap allocation for no benefit. Also, Box gives single ownership only; if you need shared ownership use Rc/Arc, and if you need shared mutation reach for Mutex or RefCell instead.

Deref and Deref Coercion

What does the `Deref` trait do?
  • Answer: It lets a type customize what the operator does. Without Deref, only works on plain references. When you write y on a type implementing Deref, the compiler rewrites it as (y.deref()) — call deref once to get a normal reference, then dereference that. It runs once per *, so there is no infinite recursion.
  • Example:
struct MyBox<T>(T);

impl<T> std::ops::Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }
}

let y = MyBox(5);
assert_eq!(5, *y); // == *(y.deref())
Why does `deref` return a reference `&T` instead of the value `T`?
  • Answer: If it returned T directly, the value would move out of self — the smart pointer would lose ownership of its own contents every time you dereferenced it. Returning &T borrows the inner value, which is what callers almost always want.
What is deref coercion?
  • Answer: An automatic conversion the compiler applies at function and method call sites: if T: Deref<Target = U>, then &T can be passed where &U is expected — and it chains. This is why &String works as &str, and &Box<String> also works as &str (two coercion steps). It saves you from typing &***x everywhere.
  • Example:
fn hello(name: &str) { println!("Hello, {name}!"); }

let m = MyBox(String::from("Rust"));
hello(&m);   // &MyBox<String> -> &String -> &str — compiles
What is `DerefMut` and how does it relate to `Deref`?
  • Answer: The mutable version of the trait. Implementing DerefMut provides deref_mut() -> &mut T and powers *y = value and mutable coercions like &mut String -> &mut str. Rust picks Deref or DerefMut based on whether the context needs immutable or mutable access.

The `Drop` Trait

What is the `Drop` trait used for?
  • Answer: It defines code that runs automatically when a value goes out of scope — releasing heap memory (Box frees its allocation), closing files and sockets, releasing locks (MutexGuard), or decrementing a reference count (Rc). The compiler inserts the call for you, so cleanup can't be forgotten like it can in languages where you free resources manually.
  • Example:
struct CustomSmartPointer { data: String }

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping `{}`!", self.data);
    }
}
In what order are values dropped?
  • Answer: Local variables drop in reverse order of creation — the last value created is the first dropped. So d created after c drops before c. This stack-like order matches how destructors work in C++ and keeps dependent resources consistent.
Why can't you call `x.drop()` directly?
  • Answer: Because Drop::drop doesn't consume the value — it takes &mut self. Rust would still run the destructor again automatically at the end of the scope, cleaning up the same value twice: a classic double-free bug. That's why the compiler flatly rejects c.drop() with "explicit destructor calls not allowed."
How do you drop a value early, and when would you want to?
  • Answer: Call std::mem::drop(x) (it's in the prelude — just drop(x)). It takes ownership of x and drops it immediately, so the destructor can't run again later. Common reasons: release a MutexGuard lock early so another thread can proceed, close a file deterministically, or end a borrow for the borrow checker.
  • Example:
let c = CustomSmartPointer { data: String::from("some data") };
drop(c); // destructor runs right here, not at end of scope

`Rc<T>` — Shared Ownership

What problem does `Rc<T>` solve?
  • Answer: Sometimes one value genuinely has multiple owners — a graph node pointed at by many edges, or a shared tail of two lists — and you can't know at compile time which owner finishes last. Rc<T> (reference counted) tracks how many owning pointers exist; the inner data is dropped only when the count reaches zero, so it is never freed while someone still uses it.
  • Real-life example: A TV in a shared flat. Roommates come and go freely; whoever leaves last switches it off. You never turn it off while someone is still watching.
How does `Rc::clone` differ from a normal `.clone()`?
  • Answer: It doesn't copy the data at all — it just increments the reference count, an O(1) operation. The Rust convention is to call it as Rc::clone(&a) rather than a.clone() so readers can instantly tell "cheap count bump" apart from a potentially expensive deep clone when hunting for performance issues.
  • Example:
use std::rc::Rc;

let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));   // count 1 -> 2, b shares a's tail
let c = Cons(4, Rc::clone(&a));   // count 2 -> 3
What is `strong_count`, and when is the `Rc` data actually freed?
  • Answer: Rc::strong_count(&a) returns the number of owning pointers. Every Rc::clone adds one; every time an Rc goes out of scope its Drop subtracts one — you never decrement manually. When the count hits zero the inner value is dropped and the heap memory freed. There is also a weak_count for Weak<T> references, which does not keep the data alive.
Why is `Rc<T>` restricted to a single thread?
  • Answer: Its counter is a plain integer updated with normal arithmetic. If two threads cloned Rcs simultaneously they could race on the count — a lost update could leave the count stuck above zero (leak) or drop it to zero early (use-after-free). The fix is Arc<T>, which uses atomic increments safe across threads, at a small performance cost.

`RefCell<T>` and Interior Mutability

What is the interior mutability pattern?
  • Answer: Mutating the inside of a value through an API that looks immutable from the outside. RefCell<T> holds the data privately; its methods take &self yet can change the contents because the borrow rules are checked at runtime inside the type, not by the compiler. The outer type stays immutable; the mutation happens "in the interior."
  • Real-life example: A shared whiteboard with a sign-out marker. Anyone may look anytime, but to write you must take the single marker — and the board enforces it at the door, not when the board was installed.
How do `borrow()` and `borrow_mut()` work, and what happens if you break the rules?
  • Answer: borrow() returns Ref<T> (many can coexist); borrow_mut() returns RefMut<T> (only one, and no active Refs). These are guard objects — when the guard drops, the borrow is released. Break the rules — a second borrow_mut while one is live, or a borrow while a mutable borrow exists — and the program panics at runtime instead of failing to compile.
  • Example:
let data = RefCell::new(5);
{
    let mut x = data.borrow_mut();
    *x += 1;
}                       // RefMut dropped here, borrow released
let y = data.borrow();  // fine now — reads 6
// let z = data.borrow_mut(); while y is alive -> panic!
Why accept runtime borrow checks — isn't compile-time checking the whole point of Rust?
  • Answer: The borrow checker is deliberately conservative: if it can't prove code is safe, it rejects it — including some programs that are actually fine. RefCell is for when you know borrows are short-lived and non-overlapping but the compiler can't see it. You trade a small runtime cost and a possible panic for flexibility — and like Rc, RefCell is single-threaded only.
  • Example: the book's mock Messenger — send(&self) must record messages into sent_messages: RefCell<Vec<String>>, mutating through an immutable &self receiver so it satisfies the trait signature.
Why is `Rc<RefCell<T>>` such a common combination?
  • Answer: Because the two types cover each other's gaps: Rc gives multiple owners but only immutable access; RefCell gives mutation but only one owner. Wrap them and you get data that is both shared and mutable — the standard recipe for graphs, shared config, observer lists, and the book's cons-list-with-editable-tail examples.
Recap — `Box` vs `Rc` vs `RefCell` in one line each?
  • Answer:
  • Box<T> — one owner, heap data, compile-time borrow rules.
  • Rc<T> — many owners, immutable access only, single thread.
  • RefCell<T> — one owner, mutable or immutable borrows checked at runtime (panics on violation), single thread.

Reference Cycles and `Weak<T>`

Can Rust leak memory? How does a reference cycle do it?
  • Answer: Yes — memory leaks are considered memory safe, so the compiler doesn't prevent them. Combine Rc (shared ownership) with RefCell (mutability) and two nodes can end up owning each other: a points to b, b points back to a. Each keeps the other's count at 1 forever, so neither is ever freed even after all outside handles are gone.
  • Example:
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

// make a point back to b -> cycle: both counts stay at 2, then 1 forever
if let Some(link) = a.tail() {
    *link.borrow_mut() = Rc::clone(&b);
}
What is `Weak<T>` and how does it break a cycle?
  • Answer: Weak<T> is a non-owning reference to data inside an Rc. You create one with Rc::downgrade(&rc) — it bumps weak_count, not strong_count, so it never keeps the value alive. To use it you call weak.upgrade(), which returns Option<Rc<T>>: Some while the data lives, None after it's been dropped. A cycle that includes a weak edge breaks on its own once the strong owners go away.
  • Real-life example: Owning a flat vs knowing a friend's address. Your flat's deed keeps it yours; knowing a friend's address doesn't — if they move out, the address simply stops working.
What is the difference between `strong_count` and `weak_count`?
  • Answer: strong_count is the number of owning Rcs — only it decides when the value is dropped (at zero). weak_count tracks live Weak references for bookkeeping; it doesn't need to reach zero for the data to be freed, and any upgrade() on a leftover Weak afterwards just returns None.
Show a real pattern where `Weak` is the right tool — the parent/child tree.
  • Answer: When children should be owned by a parent but also need to look back at it: parent owns children via Rc, children hold a Weak back-pointer to the parent. Ownership flows one way only, so no cycle — when the parent drops, children drop too.
  • Example:
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,            // child -> parent: weak, non-owning
    children: RefCell<Vec<Rc<Node>>>,       // parent -> children: strong, owning
}

let child = Rc::new(Node { /* ... */ });
*child.parent.borrow_mut() = Rc::downgrade(&parent); // Weak<Node>
child.parent.borrow().upgrade()             // Option<Rc<Node>> -> Some(parent)
How do you avoid reference cycles in practice?
  • Answer: Decide which links own and which only observe: owners get Rc, back-edges/observers get Weak. Alternative designs help too — arena allocation with index lookups, or simply restructuring so graphs have a clear ownership direction. The compiler won't catch cycles for you, so tests, code review, and occasional heap profiling are your safety net.

Threads and `std::thread`

What is the difference between a process and a thread, and which threading model does Rust's std use?
  • Answer: A process is an OS-managed program instance with its own memory; threads are independent execution paths inside one process that share its memory. Rust's standard library uses the 1:1 model — every thread::spawn maps to one real OS thread, scheduled by the OS. Async tasks are a different model where many tasks ride on few threads.
How do you spawn a thread, and why is `join()` needed?
  • Answer: thread::spawn(closure) starts a new thread and returns a JoinHandle. If main returns, all spawned threads are killed immediately whether they finished or not — a spawned thread might not even run once. Calling handle.join() blocks the current thread until that worker finishes, guaranteeing its work completes before exit.
  • Example:
let handle = thread::spawn(|| {
    for i in 1..10 { println!("spawned: {i}"); }
});
handle.join().unwrap(); // main waits here until spawned thread is done
Why does WHERE you call `join()` matter?
  • Answer: join blocks the calling thread at that exact point. Call it before your main loop and everything becomes sequential — the spawned thread finishes first, then main runs. Call it after and the two truly interleave. Placement, not just existence, decides what runs in parallel — a classic interview subtlety.
Why do thread closures almost always need the `move` keyword?
  • Answer: A spawned thread can outlive the function that started it, so its closure must be 'static — it can't hold references to stack locals (the classic "closure may outlive the current function, but it borrows v" error). move forces the closure to take ownership of everything it captures, transferring that data into the thread so nothing can dangle.
  • Example:
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("vector: {v:?}"); // v is owned by the thread now
});
// v can't be used in main anymore — it moved
What can go wrong with threads even in Rust?
  • Answer: Three classic issues: race conditions (threads touch data in an inconsistent order), deadlocks (two threads wait on each other forever), and heisenbugs (failures that appear only under certain scheduling). Rust's ownership + Send/Sync rules eliminate data races at compile time, but the compiler can't prevent logic-level deadlocks or ordering surprises — those are still on you.

Message Passing with Channels

What is `mpsc::channel`, and what do `tx`/`rx` mean?
  • Answer: std::sync::mpsc::channel() creates a channel — mpsc stands for multiple producer, single consumer. It returns a tuple (tx, rx): tx is the transmitter/sender (you can clone it for many producers) and rx is the single receiver that consumes the stream. A channel is closed when either end is dropped.
  • Real-life example: A river. Many small streams (senders) feed into it upstream; everything dropped in any stream ends up at the same downstream point (the receiver).
How do `send` and `recv` behave? What about `try_recv`?
  • Answer: tx.send(v) pushes v into the channel and returns Result — Err if the receiver was dropped, so you know nobody's listening. rx.recv() blocks the calling thread until a message arrives, and returns Err once the channel is closed and drained. rx.try_recv() never blocks — Ok(msg) if one's waiting, Err immediately otherwise, useful for polling loops that do other work between checks.
  • Example:
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    tx.send(String::from("hi")).unwrap();
});

let received = rx.recv().unwrap(); // blocks until "hi" arrives
println!("Got: {received}");
Why does `send` take ownership of the value?
  • Answer: Once the value is handed to another thread, that thread can mutate or drop it — letting the sender keep using it would create exactly the race channels exist to prevent. Moving ownership through the channel enforces the slogan: "Don't communicate by sharing memory; share memory by communicating." Trying to use val after tx.send(val) is a compile error.
How do you get multiple producers sending into one channel?
  • Answer: Clone the transmitter — let tx2 = tx.clone(); — and move each clone into its own thread. Every sender's messages land in the same rx stream; the receiver can't tell (and doesn't need to know) which producer sent what.
  • Example:
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();

thread::spawn(move || { tx.send("from thread 1").unwrap(); });
thread::spawn(move || { tx2.send("from thread 2").unwrap(); });
How do you receive "everything until all senders are done"?
  • Answer: Treat rx as an iterator — for received in rx { ... } yields each message and ends the loop automatically once every tx is dropped. The classic bug: a sender still alive in scope, so the loop blocks forever waiting for a next message that never comes — the receiver only knows the channel is closed when ALL transmitters are gone.
  • Example:
for received in rx {
    println!("Got: {received}");
} // loop exits when every tx has been dropped

Shared State — `Mutex` and `Arc`

What is a `Mutex` and how do you use `Mutex<T>`?
  • Answer: Mutual exclusion — only one thread can touch the data at a time. m.lock() blocks until it's your turn, then returns a MutexGuard: a smart pointer that derefs to the inner data. The type system forces this — a Mutex<i32> is not an i32, so you literally cannot access the data without locking first. You can't forget to lock, and you can't forget to unlock either.
  • Example:
let m = Mutex::new(5);
{
    let mut num = m.lock().unwrap();
    *num = 6;
} // MutexGuard drops here -> lock released automatically
How is the lock released — what if a thread forgets to unlock?
  • Answer: It can't forget: MutexGuard implements Drop, so when the guard goes out of scope the lock releases automatically (RAII). No manual unlock, no forgotten-release bugs that plague mutexes in C/C++. Want the lock released early? drop(guard) or end the scope — same Drop mechanics as before.
Why do you need `Arc<Mutex<T>>` to share a counter between threads?
  • Answer: Each of 10 threads needs its own owner handle on the shared counter — one Mutex can't be moved into 10 closures. Rc would count owners but the compiler rejects it across threads (Rc is not Send). Arc is the atomic, thread-safe refcount, so Arc<Mutex<T>> = shared ownership + serialized mutation, which is exactly what multi-threaded shared state needs.
  • Example:
let counter = Arc::new(Mutex::new(0));

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    thread::spawn(move || {
        *counter.lock().unwrap() += 1;
    });
}
// after joining all handles: counter holds 10
`Arc` vs `Rc` — same API, so what's the actual difference?
  • Answer: Only the counter. Rc uses a plain integer — faster, but races if two threads touch it. Arc (atomic reference counting) uses atomic CPU instructions for increments/decrements, safe under concurrency at a small extra cost. The compiler stops you from ever shipping an Rc across threads, so the choice is mechanical: single thread Rc, multiple threads Arc.
Can you still deadlock with `Mutex` in Rust? What is a poisoned mutex?
  • Answer: Rust prevents data races, not deadlocks — two threads locking m1 then m2 in opposite orders can still freeze each other. And if a thread panics while holding the lock, the mutex becomes "poisoned": later lock() calls return Err because the guarded data may be left in a weird state — that's why examples write lock().unwrap(). You can recover the data with into_inner() if you decide it's fine.

`Send` and `Sync`

What do `Send` and `Sync` mean — one sentence each?
  • Answer: Send = values of this type can safely move ownership to another thread. Sync = values of this type can be referenced from multiple threads at once (equivalently, &T is Send). Both are marker traits in std::marker — no methods, just compile-time facts the type system enforces.
Which everyday types are NOT `Send`/`Sync`, and why?
  • Answer:
  • Rc<T> — neither: the refcount isn't atomic, so sending or sharing it between threads could corrupt the count.
  • RefCell<T> / Cell<T> — Send (if T is) but not Sync: the runtime borrow flags aren't thread-safe, so one thread may own it but two may not look inside simultaneously.
  • Mutex<T> — both Send and Sync: that's exactly what makes it usable for shared state.
  • MutexGuard — Sync but not Send: some platforms require a mutex be unlocked by the same thread that locked it.
  • Raw pointers — neither: safety can't be proven automatically.
Will you ever implement `Send` or `Sync` manually?
  • Answer: Almost never. They are auto-traits — any type built entirely from Send/Sync parts automatically gets them, which covers ordinary structs and enums. Manual impls are unsafe because the compiler can't verify your thread-safety promises; you only do it when building a new primitive, and it belongs behind a safe API.
How do `Send` and `Sync` make concurrency "fearless"?
  • Answer: They turn thread-safety mistakes into compile errors. Try sending Rc into thread::spawn — rejected. Try sharing bare mutable data — rejected. Arc<Mutex<T>> compiles only because every piece proves Send/Sync. Once your code compiles, the sharing is sound — races, invalid references, and half-updated counts can't happen. That's the payoff slogan of the chapter: fearless concurrency.