Rust smart pointers interview questions, with answers
Ownership gives every value one owner, and most code never needs more. Smart pointers cover the cases where it does: data on the heap, data with several owners, data that must be changed through a shared reference, and data shared between threads. Interviewers ask what each smart pointer is for and what it costs, because choosing the right one shows you understand the ownership model rather than fighting it.
The answers below cover Box, Rc, RefCell, Arc, Mutex and Weak, with code compiled and run on rustc 1.98. Then take the free Rust diagnostic — ten questions across every Rust topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is Box<T> used for in Rust?
In short: Box puts a value on the heap with a single owner; it is needed for recursive types, for large values you want to move cheaply, and for trait objects.
Box::new(v) allocates v on the heap and returns an owning pointer, which frees the allocation when it is dropped. Its main uses are recursive types, which would otherwise have infinite size: List below can contain another List only through a Box, which has a fixed pointer size. It also moves large values cheaply, since only the pointer is copied, and it holds trait objects such as Box<dyn Error>, whose concrete size is unknown at compile time. A Box behaves like the value it points to, because it implements Deref, so sum can match on the inner List directly.
enum List { Node(i32, Box<List>), End, } fn sum(l: &List) -> i32 { match l { List::Node(v, next) => v + sum(next), List::End => 0, } } use List::*; let t = Node(2, Box::new(End)); let l = Node(1, Box::new(t)); println!("{}", sum(&l)); // 3
2.How does Rc<T> work in Rust?
In short: Rc is a reference-counted pointer that lets several owners share one value; cloning it increments a count, and the value is freed when the last Rc is dropped.
Some data structures have no single owner, such as a node shared by two graphs or a configuration used by many components. Rc::new(v) puts v on the heap together with a count of owners. Rc::clone(&a) creates another pointer to the same value and increments the count, without copying the value, and dropping an Rc decrements it; at zero, the value is freed. Rc gives only shared, read-only access to its contents, so mutation needs a RefCell inside it. It is intended for single-threaded code, and reference cycles between Rc values are never freed, which Weak pointers exist to break.
use std::rc::Rc; let a = Rc::new(vec![1, 2]); let b = Rc::clone(&a); let n = Rc::strong_count(&a); println!("{n} {:?}", b); // 2 [1, 2]
3.What is interior mutability in Rust, and what does RefCell do?
In short: Interior mutability means changing data through a shared reference; RefCell allows it by checking the borrowing rules at run time instead of compile time.
The normal rules forbid changing a value through a &T. Some designs need exactly that, such as a shared log that several owners append to, or a cache filled in lazily. RefCell<T> provides it: borrow() and borrow_mut() hand out references while the RefCell counts them, enforcing one writer or many readers as the program runs. Combined as Rc<RefCell<T>>, it gives shared ownership of mutable data in single-threaded code, as the log below shows, with both handles appending to the same vector. Cell<T> is a lighter alternative for Copy values, replacing them whole without handing out references.
use std::cell::RefCell; use std::rc::Rc; fn main() { let v = Vec::new(); let c = RefCell::new(v); let log = Rc::new(c); let l2 = Rc::clone(&log); l2.borrow_mut().push("a"); log.borrow_mut().push("b"); let items = log.borrow(); println!("{items:?}"); // ["a", "b"] }
4.How do you share data between threads in Rust?
In short: Move owned data into the thread, share read-only data through an Arc, and add a Mutex or channel when threads must change or exchange data.
thread::spawn takes a closure that must own everything it uses, because the thread may outlive the function that started it; move closures do that. To give several threads the same data, wrap it in an Arc, an atomically reference-counted pointer, and move a clone into each thread, as below: the thread doubles the shared value while main keeps its own handle. For data that threads must change, put a Mutex or RwLock inside the Arc, or send messages over a channel from std::sync::mpsc. The compiler checks all of this through the Send and Sync traits, so sharing a type that is not thread-safe does not compile.
use std::sync::Arc; use std::thread; fn main() { let cfg = Arc::new(5); let c2 = Arc::clone(&cfg); let f = move || *c2 * 2; let h = thread::spawn(f); let n = h.join().unwrap(); println!("{n} {cfg}"); // 10 5 }
5.How does a Mutex work in Rust?
In short: A Mutex<T> owns the data it protects; lock returns a guard that gives access to the data and releases the lock automatically when the guard is dropped.
In many languages a mutex sits next to the data it protects, and nothing stops code touching the data without locking. Rust's Mutex<T> contains the data, so the only way to reach it is lock(), which blocks until the lock is free and returns a MutexGuard that dereferences to the data. The lock is released when the guard is dropped, at the end of the statement for a temporary guard, as in the push below, or at the end of the scope for a named one, so forgetting to unlock is impossible. lock returns a Result because a thread that panicked while holding the lock leaves the mutex poisoned.
use std::sync::Mutex; let m = Mutex::new(vec![1]); m.lock().unwrap().push(2); let g = m.lock().unwrap(); println!("{:?}", *g); // [1, 2]
6.What is a Weak reference in Rust, and when do you need one?
In short: Weak is a non-owning pointer to an Rc's value; it does not keep the value alive, and upgrade returns Some while the value exists and None after it is freed.
Rc keeps a value alive as long as any strong pointer exists, so two values that point at each other through Rc form a cycle that is never freed. Weak breaks such cycles: Rc::downgrade creates a Weak pointer that is counted separately and does not prevent the value from being dropped. To use it, call upgrade, which returns Some(Rc) if the value still exists and None if every strong owner is gone. The Rc that upgrade returns is itself a strong owner, which is why the code drops u as well as strong before the second upgrade returns None. Trees use this pattern: parents own their children through Rc, and children refer back to their parent through Weak. Arc has a matching sync::Weak.
use std::rc::{Rc, Weak}; fn main() { let strong = Rc::new(1); let weak: Weak<i32> = Rc::downgrade(&strong); let u = weak.upgrade(); println!("{u:?}"); // Some(1) drop(u); drop(strong); let u = weak.upgrade(); println!("{u:?}"); // None }
How the diagnostic asks it
One question from the Rust bank, exactly as a sitting would show it. The bank has 4 on smart pointers & concurrency and 30 across Rust.
What does this Rust program print?
use std::rc::Rc; fn main() { let a = Rc::new(5); let b = Rc::clone(&a); { let _c = Rc::clone(&a); println!("{}", Rc::strong_count(&a)); } println!("{} {}", Rc::strong_count(&b), *b); }
- 13, then 3 5
- 21, then 1 5
- 3It does not compile: a was moved into b
- 43, then 2 5correct
Rc gives a value several owners by counting them. Rc::clone does not copy the 5; it creates another pointer to the same allocation and increments the strong count. With a, b and _c alive, the count is 3. _c is dropped at the end of the inner block, which decrements the count to 2, and b still points at 5, so the output is 3, then 2 5. 3, then 3 ignores the drop. 1 assumes each clone is a separate allocation. Rc::clone takes a reference to a, so a is not moved. The value is freed only when the count reaches 0.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Rust questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.