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.
Markdown source: courses/rust-pointers-concurrency/index.md · Based on The 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*ptrworks like a regular reference) andDrop(so cleanup runs automatically when it goes out of scope). References&Tonly 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 ofRc.Mutex<T>/RwLock<T>— safe shared mutation between threads.Weak<T>— a non-owning reference toRc/Arcdata, 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
Boxitself 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:
- The type's size can't be known at compile time — recursive types like linked lists or trees, where
Boxgives the field a fixed pointer size. - You have a large value and want to transfer ownership without copying the data — only the pointer moves, the heap data stays put.
- 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)theListfield contains anotherList, which contains anotherList— so the size never stops growing and the compiler reports "recursive type has infinite size." Wrapping the field inBox<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
i32adds a heap allocation for no benefit. Also,Boxgives single ownership only; if you need shared ownership useRc/Arc, and if you need shared mutation reach forMutexorRefCellinstead.
Deref and Deref Coercion
What does the `Deref` trait do?
- Answer: It lets a type customize what the
operator does. WithoutDeref,only works on plain references. When you writeyon a type implementingDeref, the compiler rewrites it as(y.deref())— callderefonce 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
Tdirectly, the value would move out ofself— the smart pointer would lose ownership of its own contents every time you dereferenced it. Returning&Tborrows 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&Tcan be passed where&Uis expected — and it chains. This is why&Stringworks as&str, and&Box<String>also works as&str(two coercion steps). It saves you from typing&***xeverywhere. - 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
DerefMutprovidesderef_mut() -> &mut Tand powers*y = valueand mutable coercions like&mut String -> &mut str. Rust picksDereforDerefMutbased 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 (
Boxfrees 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
dcreated aftercdrops beforec. 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::dropdoesn'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 rejectsc.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 — justdrop(x)). It takes ownership ofxand drops it immediately, so the destructor can't run again later. Common reasons: release aMutexGuardlock 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
`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&selfyet 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()returnsRef<T>(many can coexist);borrow_mut()returnsRefMut<T>(only one, and no activeRefs). These are guard objects — when the guard drops, the borrow is released. Break the rules — a secondborrow_mutwhile 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.
RefCellis 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 likeRc,RefCellis single-threaded only. - Example: the book's mock
Messenger—send(&self)must record messages intosent_messages: RefCell<Vec<String>>, mutating through an immutable&selfreceiver 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:
Rcgives multiple owners but only immutable access;RefCellgives 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) withRefCell(mutability) and two nodes can end up owning each other:apoints tob,bpoints back toa. 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 anRc. You create one withRc::downgrade(&rc)— it bumpsweak_count, notstrong_count, so it never keeps the value alive. To use it you callweak.upgrade(), which returnsOption<Rc<T>>:Somewhile the data lives,Noneafter 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_countis the number of owningRcs — only it decides when the value is dropped (at zero).weak_counttracks liveWeakreferences for bookkeeping; it doesn't need to reach zero for the data to be freed, and anyupgrade()on a leftoverWeakafterwards just returnsNone.
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 aWeakback-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 getWeak. 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::spawnmaps 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 aJoinHandle. Ifmainreturns, all spawned threads are killed immediately whether they finished or not — a spawned thread might not even run once. Callinghandle.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:
joinblocks 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 borrowsv" error).moveforces 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/Syncrules 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):txis the transmitter/sender (you cancloneit for many producers) andrxis 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)pushesvinto the channel and returnsResult—Errif the receiver was dropped, so you know nobody's listening.rx.recv()blocks the calling thread until a message arrives, and returnsErronce the channel is closed and drained.rx.try_recv()never blocks —Ok(msg)if one's waiting,Errimmediately 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
valaftertx.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 samerxstream; 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
rxas an iterator —for received in rx { ... }yields each message and ends the loop automatically once everytxis 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
`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,&TisSend). Both are marker traits instd::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(ifTis) but notSync: the runtime borrow flags aren't thread-safe, so one thread may own it but two may not look inside simultaneously.Mutex<T>— bothSendandSync: that's exactly what makes it usable for shared state.MutexGuard—Syncbut notSend: 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/Syncparts automatically gets them, which covers ordinary structs and enums. Manual impls areunsafebecause 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
Rcintothread::spawn— rejected. Try sharing bare mutable data — rejected.Arc<Mutex<T>>compiles only because every piece provesSend/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.