Rust smart pointers
What a smart pointer is, and when Box<T> earns its place
A smart pointer is a struct that owns its data and adds behavior through the Deref and Drop traits. A plain reference only borrows; a smart pointer holds the value and cleans it up when it goes away.
Box<T> is the simplest: one owner, the value on the heap, a single pointer on the stack. It adds no counting and no atomics, only the allocation. Three jobs make it worth reaching for.
// Recursive type: without indirection the compiler cannot size it.
enum List {
Cons(i32, Box<List>), // Box gives a fixed pointer-size slot
Nil,
}
// enum List { Cons(i32, List), Nil } // error[E0072]: recursive type has infinite size
Box also holds a trait object (Box<dyn Trait>) and moves a large value without copying its bytes around. Putting a lone i32 on the heap for speed is not one of its uses.
Rc<T>: several owners of read-only data
When one value needs many owners in a single thread, Rc<T> reference-counts them. Rc::clone bumps the count and hands back another handle to the same allocation. It does not deep-copy the value.
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // count 1 -> 2, an O(1) bump
println!("{}", Rc::strong_count(&a)); // 2
The value drops only when the last Rc goes away (the strong count reaches 0). An Rc gives shared, immutable access: you cannot get &mut through it.
RefCell<T> for mutation behind a shared handle
To mutate what an Rc shares, wrap it in RefCell<T>. That is interior mutability: borrow_mut() hands out a mutable guard through a &self receiver. The borrow rule still holds (many readers or one writer), enforced at run time rather than at compile time.
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(5));
let other = Rc::clone(&shared);
*shared.borrow_mut() += 10; // every owner now sees 15
assert_eq!(*other.borrow(), 15);
Wrong "RefCell turns off the borrow checker." It runs the same check, only later, and a violation panics instead of failing the build.
Deref and Drop
*boxed works because Box implements Deref, so *y expands to *(y.deref()). Drop runs cleanup when a value leaves scope, the RAII pattern. To free a value early you call the free function std::mem::drop(value), never value.drop().
Rc is single-threaded, Arc pays for atomics
Rc<T> keeps a plain, non-atomic count, so it is neither Send nor Sync. Move one into another thread and the compiler stops you at build time (E0277, Rc<...> cannot be sent between threads safely), because two threads racing on the count could double-free or leak.
Arc<T> is the same shape with an atomic count, so it is Send + Sync when T: Send + Sync. Atomic increments cost more than ordinary loads, so reach for Arc only when the allocation crosses a thread boundary. Neither type grants mutation: cross-thread mutation needs Arc<Mutex<T>> or Arc<RwLock<T>>, not Arc<RefCell<T>> (a RefCell is not Sync).
RefCell borrows are checked at run time
borrow() panics if the value is already mutably borrowed; borrow_mut() panics if it is borrowed at all. The panic carries a BorrowError or BorrowMutError (the exact message text shifts between toolchains, the type does not). The Ref and RefMut guards release the borrow when they drop, so the trap is a guard that outlives where you thought it did:
let cell = RefCell::new(vec![1]);
let a = cell.borrow_mut();
let b = cell.borrow_mut(); // compiles, panics here: BorrowMutError
This builds cleanly and only blows up on the path that re-enters. Where re-entrancy is possible, try_borrow() and try_borrow_mut() return a Result instead of panicking. Cell<T> sidesteps the question for Copy values: it moves values in and out and never hands out a reference, so it tracks nothing and cannot panic.
Reference cycles leak, and Weak breaks them
Two Rc<RefCell<Node>> values that point at each other each hold the other's strong count at 1 or more, so neither reaches 0 and neither drops. The nodes leak, and a leak is memory-safe: Rust prevents use-after-free and double-free; it does not prevent leaks.
Weak<T> breaks the cycle. Rc::downgrade produces one and bumps the weak count, so it does not keep the value alive. You cannot read through a Weak directly; you call upgrade(), which returns Some(Rc) while the value lives and None once it is gone.
struct Node {
parent: RefCell<Weak<Node>>, // non-owning back-pointer
children: RefCell<Vec<Rc<Node>>>, // owning
}
Only the strong count reaching 0 runs the value's destructor; the weak count keeps the backing allocation alive to hold the counters.
Deref coercion and drop order
Deref coercion inserts deref() calls at compile time to turn &Box<String> into &str, at zero runtime cost. deref() returns a reference to the inner data, so nothing is moved out. The compiler will go &mut to &, but never & to &mut: an immutable reference may be aliased, and a &mut must be unique.
Drop runs in reverse declaration order for locals, so the last binding drops first. Calling value.drop() by hand is rejected (E0040): the value would still be dropped at end of scope, a double free. std::mem::drop(value) takes the value by move, so the destructor runs early and the end-of-scope drop does not repeat.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.