What Are Smart Pointers?
Smart pointers are data structures that act like pointers but also have
additional metadata and capabilities. They own the data they point to and manage its lifecycle.
The most common smart pointers in Rust are Box, Rc, Arc,
and RefCell.
Box<T> — Heap Allocation
fn main() {
// Box puts data on the heap
let b = Box::new(5);
println!("b = {b}"); // Deref makes it transparent
// Use cases for Box:
// 1. Recursive types (unknown size at compile time)
// 2. Large data you want on the heap
// 3. Trait objects
// Recursive type example
#[derive(Debug)]
enum List {
Cons(i32, Box),
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
println!("{:?}", list);
// Large struct on heap to avoid stack overflow
let big_array = Box::new([0u8; 1_000_000]);
println!("Big array len: {}", big_array.len());
}
// Trait objects with Box
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn speak(&self) -> &str { "Woof!" } }
impl Animal for Cat { fn speak(&self) -> &str { "Meow!" } }
fn get_animal(kind: &str) -> Box {
match kind {
"dog" => Box::new(Dog),
_ => Box::new(Cat),
}
}
Rc<T> — Reference Counting
use std::rc::Rc;
fn main() {
// Rc allows multiple owners of the same data
// (single-threaded only!)
let data = Rc::new(vec![1, 2, 3]);
println!("Reference count: {}", Rc::strong_count(&data)); // 1
let clone1 = Rc::clone(&data); // Increment count, NOT deep copy
println!("Reference count: {}", Rc::strong_count(&data)); // 2
let clone2 = Rc::clone(&data);
println!("Reference count: {}", Rc::strong_count(&data)); // 3
// All three variables point to the same data
println!("{:?}", data);
println!("{:?}", clone1);
println!("{:?}", clone2);
drop(clone2);
println!("After drop: {}", Rc::strong_count(&data)); // 2
// Rc is read-only! You can't mutate the data.
// For mutation, combine with RefCell (next section)
}
RefCell<T> — Interior Mutability
Interior Mutability Pattern
RefCell<T> enforces borrowing rules at runtime instead
of compile time. This lets you mutate data even when there are immutable references to it,
as long as the rules are respected at runtime (or it panics).
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// borrow() — immutable borrow (checked at runtime)
println!("{:?}", data.borrow());
// borrow_mut() — mutable borrow (checked at runtime)
data.borrow_mut().push(4);
println!("{:?}", data.borrow());
// Combining Rc + RefCell = multiple owners with mutation
let shared = Rc::new(RefCell::new(0));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
*a.borrow_mut() += 10;
*b.borrow_mut() += 20;
println!("Shared value: {}", shared.borrow()); // 30
// PANIC if you violate borrowing rules at runtime:
// let borrow1 = data.borrow();
// let borrow2 = data.borrow_mut(); // PANIC: already borrowed
}
Arc<T> — Atomic Reference Counting
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc = Rc but thread-safe (uses atomic operations)
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut num = data.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *data.lock().unwrap()); // 10
}
// When to use which:
// Box — Single owner, heap allocation
// Rc — Multiple owners, single-threaded
// Arc — Multiple owners, multi-threaded
// RefCell — Interior mutability (runtime borrow checking)
// Mutex — Interior mutability (thread-safe)
Deref and Drop Traits
use std::ops::Deref;
// Custom smart pointer
struct MyBox(T);
impl MyBox {
fn new(x: T) -> MyBox {
MyBox(x)
}
}
impl Deref for MyBox {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl Drop for MyBox {
fn drop(&mut self) {
println!("Dropping MyBox with value: {:?}", self.0);
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let x = MyBox::new(String::from("Rust"));
// Deref coercion: &MyBox -> &String -> &str
hello(&x);
// Explicit early drop
let y = MyBox::new(42);
drop(y); // Calls Drop::drop immediately
println!("y has been dropped");
} // x is dropped here automatically
Key Takeaways
- ✅
Box<T>allocates on the heap — essential for recursive types and trait objects - ✅
Rc<T>enables multiple ownership via reference counting (single-threaded) - ✅
Arc<T>is the thread-safe version of Rc using atomic operations - ✅
RefCell<T>moves borrow checking to runtime, enabling interior mutability - ✅
Derefenables transparent dereferencing;Dropruns cleanup code automatically