Smart pointers
Box, Rc, Arc, RefCell, Mutex, and the RAII patterns that make heap allocation explicit
A smart pointer is a struct that owns a heap allocation and behaves like a pointer through the Deref and Drop traits. The standard library ships a small set, each solving a specific ownership problem. Knowing which to reach for is most of the battle.
Box<T>: heap allocation with single ownership
let n: Box<i32> = Box::new(42);
println!("{}", *n);Box<T> puts T on the heap. The Box itself is a pointer on the stack. When the Box is dropped, the T is dropped and the heap memory is freed.
The most common reason to reach for Box is to make a recursive type sized:
// Won't compile: infinite size
enum List {
Cons(i32, List),
Nil,
}
// Works: Box gives a pointer-sized indirection
enum List {
Cons(i32, Box<List>),
Nil,
}The other common use is trait objects: Box<dyn Trait>. More on that below.
Rc<T>: single-threaded shared ownership
use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("count: {}", Rc::strong_count(&a)); // 3Rc<T> is reference-counted. Each clone bumps the count; each drop decrements it. When the count hits zero, the value is dropped.
Rc is single-threaded only. It uses a non-atomic counter, which is fast but not Send.
Cycles leak. If a holds an Rc<B> and b holds an Rc<A>, neither count ever hits zero. Break the cycle with Weak<T>:
use std::rc::{Rc, Weak};
struct Node {
parent: Weak<Node>,
children: Vec<Rc<Node>>,
}Weak does not contribute to the count. Upgrade with .upgrade() to get an Option<Rc<T>>.
Arc<T>: thread-safe shared ownership
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);
let data2 = Arc::clone(&data);
std::thread::spawn(move || {
println!("{:?}", data2);
}).join().unwrap();Arc<T> is Rc<T> with atomic reference counting. It is Send and Sync when T is. The atomic op makes it a few cycles slower than Rc, which is essentially never a real cost.
Use Arc whenever you cross a thread boundary. Use Rc only when you have measured that the contention does not exist and you want the speed.
RefCell<T> and Cell<T>: interior mutability
The borrow checker normally requires &mut T for any mutation. RefCell<T> shifts that check from compile time to runtime. You can mutate through a &RefCell<T> by calling .borrow_mut(), which returns a RefMut<T> guard.
use std::cell::RefCell;
let v = RefCell::new(vec![1, 2, 3]);
v.borrow_mut().push(4);
println!("{:?}", v.borrow());If two borrow_muts overlap, the second panics. That is the trade: you get mutation without an exclusive reference, you accept runtime panics if the rules are broken.
Cell<T> is similar but for Copy types. No guards, just .get() and .set(). Faster, less flexible.
The combination Rc<RefCell<T>> is the classic "shared mutable state, single-threaded" pattern. Use it sparingly; if the structure is genuinely tree-shaped, prefer a plain &mut.
Mutex<T> and Arc<Mutex<T>>: thread-safe interior mutability
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (0..10).map(|_| {
let counter = Arc::clone(&counter);
std::thread::spawn(move || {
let mut g = counter.lock().unwrap();
*g += 1;
})
}).collect();
for h in handles { h.join().unwrap(); }
println!("{}", counter.lock().unwrap());Mutex<T> enforces exclusive access at runtime, just like RefCell<T>, but across threads. Pair it with Arc to share the ownership.
Mutex<T> poisons if a thread panics while holding the lock. .lock() returns a Result; the Err variant gives access to the poisoned data if you want it. In production code, deal with this explicitly rather than .unwrap()-ing.
Premature Arc<Mutex<T>>
The most common smart-pointer mistake in agent-written Rust is reaching for Arc<Mutex<T>> when a simpler primitive would do.
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0u64));
for _ in 0..1000 {
let c = Arc::clone(&counter);
std::thread::spawn(move || {
let mut g = c.lock().unwrap();
*g += 1;
});
}use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
let counter = Arc::new(AtomicU64::new(0));
for _ in 0..1000 {
let c = Arc::clone(&counter);
std::thread::spawn(move || {
c.fetch_add(1, Ordering::Relaxed);
});
}Atomics for Copy primitives. Channels (mpsc) for ownership transfer. &mut when you can split. Arc<Mutex<T>> only when you genuinely need shared mutable access to a non-Copy aggregate.
The Deref trait and deref coercion
Deref is what lets *box give you the inner value, and what lets you pass &String to a function expecting &str. The compiler chains Deref impls until the target type matches.
fn takes_str(s: &str) { println!("{}", s); }
let s: String = String::from("hello");
takes_str(&s); // &String -> &str via DerefMost of the time you do not implement Deref yourself. The places where you should: newtype wrappers that genuinely behave as the inner type, smart pointers, and only when the conversion is "this is the thing, just with extra context."
Deref::deref returns &Self::Target. There is also DerefMut::deref_mut for the mutable path.
The Drop trait — RAII
Drop::drop runs when a value goes out of scope. This is how Rust handles file closing, lock release, allocation freeing, all without a garbage collector.
struct LogGuard { name: String }
impl Drop for LogGuard {
fn drop(&mut self) {
println!("exiting {}", self.name);
}
}
fn work() {
let _g = LogGuard { name: "work".into() };
// ... do stuff ...
} // _g dropped here, message printsYou almost never call .drop() directly. To drop early, use std::mem::drop(val). The Drop trait runs automatically, in reverse order of construction, when the scope ends.
This is the foundation of guard patterns: MutexGuard, RefMut, File, TcpStream. All clean up on drop.