Smart Pointers in Rust
Understand Box<T>, Rc<T>, Arc<T>, RefCell<T>, and interior mutability patterns in Rust.
Box<T> — Heap Allocation
By default, Rust allocates values on the stack, which is fast but requires knowing the size at compile time. Box<T> moves a value to the heap and stores a fixed-size pointer on the stack. The three main reasons to reach for Box are: you need a value on the heap, you are working with trait objects (dyn Trait) whose size varies, or you are building a recursive data structure where a type would otherwise be infinite in size.
fn main() {
// Allocate an integer on the heap — useful when you need a stable address
let b = Box::new(5);
println!("b = {}", b); // Box<T> implements Deref, so it behaves like &T
// Box is dropped automatically when it goes out of scope — no manual free()
}
Recursive Types with Box
Without Box, a recursive type like a linked list or tree would have infinite size — the compiler cannot determine how much stack space to allocate. Wrapping the recursive field in Box gives it a known, fixed size (one pointer).
// Without Box, this would be an error: "recursive type has infinite size"
#[derive(Debug)]
enum List {
Cons(i32, Box<List>), // Box gives the recursive field a known size
Nil,
}
fn main() {
// Build a linked list: 1 -> 2 -> 3 -> Nil
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
println!("{:?}", list);
}
Trait Objects with Box
Box<dyn Trait> is how you store values of different concrete types that share a common interface. The concrete type is heap-allocated and the Box holds a fat pointer (data pointer + vtable pointer). This enables heterogeneous collections and runtime polymorphism.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
fn name(&self) -> &str { "circle" }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn name(&self) -> &str { "rectangle" }
}
fn main() {
// Different concrete types stored in the same Vec via trait objects
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Rectangle { width: 4.0, height: 5.0 }),
Box::new(Circle { radius: 1.5 }),
];
for shape in &shapes {
println!("{}: area = {:.2}", shape.name(), shape.area());
}
}
Rc<T> — Shared Ownership (Single-Threaded)
Rust’s ownership model normally allows only one owner per value. Rc<T> (Reference Counted) breaks that rule by maintaining a count of how many Rc handles point to the same heap allocation. The value is dropped only when the last handle is dropped. This is useful in graph-like data structures, caches, and event systems where multiple parts of the code need to share a value’s lifetime.
use std::rc::Rc;
fn main() {
let value = Rc::new(String::from("shared data"));
// Rc::clone creates another handle — does not clone the String, just increments the count
let handle1 = Rc::clone(&value);
let handle2 = Rc::clone(&value);
println!("reference count: {}", Rc::strong_count(&value)); // 3
println!("{}", value); // "shared data"
println!("{}", handle1); // same allocation
println!("{}", handle2); // same allocation
drop(handle1);
println!("after drop: {}", Rc::strong_count(&value)); // 2
// value is dropped when the last Rc handle goes out of scope
}
Rc<T> is not Send — it cannot be shared across threads. Use Arc<T> for multi-threaded scenarios.
Arc<T> — Shared Ownership (Multi-Threaded)
Arc<T> (Atomically Reference Counted) is the thread-safe version of Rc<T>. The reference count is updated with atomic CPU instructions rather than plain integer operations, which makes it safe to clone and drop Arc handles from multiple threads simultaneously. The slight overhead of atomic operations is the price for thread safety.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for _ in 0..3 {
// Clone the Arc to give each thread its own handle to the same data
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("sum: {}", data.iter().sum::<i32>());
}));
}
for h in handles { h.join().unwrap(); }
// data is dropped here — the Vec is freed when the last Arc handle is gone
}
RefCell<T> — Interior Mutability
Rust’s borrow checker enforces at compile time that you cannot have a mutable reference when shared references exist. RefCell<T> defers this check to runtime, enabling mutation through a shared reference. This is called interior mutability and is useful when the compiler cannot statically verify that borrows are safe but you know at runtime they will be.
The trade-off: if you violate the rules (two mutable borrows, or a mutable borrow while a shared borrow exists), RefCell panics at runtime instead of giving a compile error.
use std::cell::RefCell;
fn main() {
// Shared reference (&T) to the RefCell — but we can still mutate through it
let data = RefCell::new(vec![1, 2, 3]);
// borrow() gives a shared reference — like &T
println!("{:?}", data.borrow());
// borrow_mut() gives a mutable reference — like &mut T
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
// RUNTIME PANIC if you try to borrow mutably while a borrow is active:
// let _shared = data.borrow();
// let _mutable = data.borrow_mut(); // panics: already borrowed
}
Rc<RefCell<T>> — Shared Mutable State
Combining Rc and RefCell is the standard single-threaded pattern for shared mutable state. Rc provides shared ownership (multiple owners), and RefCell provides interior mutability (mutation through shared references). Together they let multiple parts of a program share and mutate the same data without fighting the borrow checker.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: Vec<Rc<RefCell<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node { value, children: vec![] }))
}
fn add_child(parent: &Rc<RefCell<Node>>, child: Rc<RefCell<Node>>) {
parent.borrow_mut().children.push(child);
}
}
fn main() {
let root = Node::new(1);
let child1 = Node::new(2);
let child2 = Node::new(3);
Node::add_child(&root, Rc::clone(&child1));
Node::add_child(&root, Rc::clone(&child2));
println!("root value: {}", root.borrow().value);
println!("children: {}", root.borrow().children.len()); // 2
// child1 can still be accessed independently even after being added to root
child1.borrow_mut().value = 20;
println!("child1 value via root: {}", root.borrow().children[0].borrow().value); // 20
}
Cell<T> — Copy-Type Interior Mutability
Cell<T> is a lighter alternative to RefCell that works only with Copy types. Instead of giving out references (which would require borrow tracking), Cell copies values in and out. There is no runtime borrow check and no possibility of panic — the restriction to Copy types is what makes it safe.
use std::cell::Cell;
struct Counter {
count: Cell<u32>, // mutable even through a shared reference
}
impl Counter {
fn new() -> Self { Self { count: Cell::new(0) } }
// Takes &self (shared reference) but still mutates count — no &mut self needed
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
fn get(&self) -> u32 {
self.count.get()
}
}
fn main() {
let counter = Counter::new();
counter.increment();
counter.increment();
counter.increment();
println!("count: {}", counter.get()); // 3
}
Smart Pointer Comparison
| Type | Ownership | Thread-safe | Mutation | Use case |
|---|---|---|---|---|
Box<T> | Unique | Yes | Normal &mut | Heap alloc, trait objects, recursive types |
Rc<T> | Shared | No | Immutable by default | Single-threaded shared ownership |
Arc<T> | Shared | Yes | Immutable by default | Multi-threaded shared ownership |
RefCell<T> | Unique | No | Interior (runtime check) | Mutation through &T, single-threaded |
Cell<T> | Unique | No | Interior (Copy only) | Simple counters/flags through &T |
Rc<RefCell<T>> | Shared | No | Interior (runtime check) | Single-threaded shared mutable state |
Arc<Mutex<T>> | Shared | Yes | Interior (blocking) | Multi-threaded shared mutable state |