Smart pointers
Smart pointers are types that wrap a pointer with additional ownership semantics. Rust’s standard library provides several: Box<T> for heap allocation with single ownership; Rc<T> and Arc<T> for shared ownership through reference counting; RefCell<T> and Mutex<T> for interior mutability — modifying the wrapped value through a shared reference. The combinations admit substantial flexibility — Rc<RefCell<T>> for single-threaded shared mutation, Arc<Mutex<T>> for multi-threaded — that the basic ownership model alone does not cover. Each smart pointer trades some safety guarantee or performance characteristic for additional flexibility; choosing the right combination is one of the principal Rust design decisions.
This page covers the principal smart pointers, the Deref and Drop mechanisms, the interior-mutability pattern, and the conventions for each. The relationship to ownership is in Ownership and borrowing; concurrency is in Concurrency.
Box<T>
Box<T> is the simplest smart pointer — single ownership of a heap allocation:
let b: Box<i32> = Box::new(42);
let n: i32 = *b; // dereference
println!("{}", *b); // 42
println!("{}", b); // 42 (auto-deref via Display)
The Box<T> heap-allocates T and admits transferring ownership through moves. When the box goes out of scope, the heap allocation is freed.
The principal uses:
Recursive types
enum List {
Cons(i32, Box<List>), // recursive; Box admits the recursion
Nil,
}
Without the Box, the type would have infinite size (each Cons would need to contain a complete List). The box introduces a known-size pointer; the actual data is on the heap.
Trait objects
trait Animal {
fn speak(&self) -> String;
}
struct Dog;
impl Animal for Dog {
fn speak(&self) -> String { String::from("woof") }
}
struct Cat;
impl Animal for Cat {
fn speak(&self) -> String { String::from("meow") }
}
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in &animals {
println!("{}", a.speak());
}
The Box<dyn Animal> admits storing different types implementing the same trait in the same collection. Treated in Traits.
Large data
let large = Box::new([0u8; 10_000_000]); // 10 MB on the heap, not the stack
For values too large to live on the stack (typically the stack is 1-8 MB), Box admits heap allocation.
Rc<T> — reference counting
Rc<T> (Reference Counted) admits shared ownership of a value. Multiple Rc<T> instances may point to the same value; the value is dropped when the last Rc is dropped.
use std::rc::Rc;
let a = Rc::new(String::from("hello"));
let b = Rc::clone(&a); // increments reference count
let c = Rc::clone(&a); // ref count: 3
println!("{}", Rc::strong_count(&a)); // 3
drop(c);
println!("{}", Rc::strong_count(&a)); // 2
The Rc::clone is cheap — it increments the reference count rather than deep-copying the value. The conventional Rust idiom uses Rc::clone(&a) (rather than a.clone()) to make the cheapness explicit.
Rc<T> is not thread-safe; the reference count is not atomic. For multi-threaded sharing, use Arc<T>.
The principal uses:
- Graph-like data structures with shared sub-trees.
- Caches where multiple owners need access to the same value.
- Trees with shared leaf nodes.
use std::rc::Rc;
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
let leaf = Rc::new(Node { value: 1, children: vec![] });
let branch = Rc::new(Node {
value: 2,
children: vec![Rc::clone(&leaf), Rc::clone(&leaf)],
});
// leaf is shared; ref count is 2
Arc<T> — atomic reference counting
Arc<T> is the thread-safe variant of Rc<T> — the reference count is atomic, so it can be shared across threads:
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..3 {
let d = Arc::clone(&data);
let h = thread::spawn(move || {
println!("thread {}: sum = {}", i, d.iter().sum::<i32>());
});
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
Arc::clone increments the atomic counter; the move admits transferring the Arc into the thread.
The conventional choice between Rc and Arc:
Rc<T>— single-threaded; faster (non-atomic counter).Arc<T>— multi-threaded; slightly slower but admits sharing across threads.
For most cases, the choice is determined by whether the data is shared across threads.
RefCell<T> — interior mutability (single-threaded)
RefCell<T> admits interior mutability — modifying the wrapped value through a shared reference (&RefCell<T>). The borrow rules are checked at runtime (not at compile time):
use std::cell::RefCell;
let cell = RefCell::new(5);
{
let r = cell.borrow(); // shared borrow
println!("{}", *r); // 5
} // borrow released
{
let mut m = cell.borrow_mut(); // exclusive borrow
*m = 10;
} // borrow released
println!("{}", *cell.borrow()); // 10
The runtime check enforces the same rules the compile-time borrow checker enforces — but at runtime, with a panic on violation:
let cell = RefCell::new(5);
let r1 = cell.borrow();
let r2 = cell.borrow_mut(); // PANIC: already borrowed
The panic happens at the second borrow; the program crashes.
The principal uses:
- Mutating fields of a struct accessed through a shared reference.
- Caches where the cache state is updated through a
&selfmethod. - Mock objects that need to record calls.
use std::cell::RefCell;
struct Counter {
count: RefCell<i32>,
}
impl Counter {
fn new() -> Self {
Counter { count: RefCell::new(0) }
}
fn increment(&self) { // takes &self, not &mut self
*self.count.borrow_mut() += 1;
}
fn value(&self) -> i32 {
*self.count.borrow()
}
}
let c = Counter::new();
c.increment(); // OK without &mut
c.increment();
println!("{}", c.value()); // 2
The pattern admits the &self-only API (which is more flexible) while still allowing internal mutation.
Cell<T> — copy-only interior mutability
Cell<T> is a simpler variant that works only for Copy types:
use std::cell::Cell;
let cell = Cell::new(5);
let n = cell.get(); // 5
cell.set(10);
let m = cell.get(); // 10
Cell<T> admits get and set but not borrow; the value is copied in and out. For Copy types, this is sufficient and avoids RefCell’s runtime check.
For non-Copy types, RefCell<T> is necessary.
Mutex<T> — multi-threaded interior mutability
Mutex<T> is the thread-safe variant of RefCell<T> — admits modifying the wrapped value through a shared reference, with synchronisation:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
let h = thread::spawn(move || {
let mut n = c.lock().unwrap(); // acquires the lock
*n += 1;
});
handles.push(h);
}
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 10
The lock() returns a Result (since the lock may be poisoned if a thread panicked while holding it). The lock is released when the guard drops.
The conventional shape is Arc<Mutex<T>> — Arc for shared ownership across threads, Mutex for synchronised mutation.
For read-mostly access, RwLock<T> admits multiple readers or one writer:
use std::sync::RwLock;
let lock = RwLock::new(5);
{
let r1 = lock.read().unwrap(); // shared read
let r2 = lock.read().unwrap(); // also shared
}
{
let mut w = lock.write().unwrap(); // exclusive write
}
The conventional choice:
Mutex<T>— for write-heavy or balanced access.RwLock<T>— for read-mostly access.
Weak<T> — non-owning shared
Weak<T> is a weak reference paired with Rc<T> or Arc<T>; admits referring to the value without preventing it from being dropped:
use std::rc::{Rc, Weak};
let strong = Rc::new(5);
let weak: Weak<i32> = Rc::downgrade(&strong);
if let Some(s) = weak.upgrade() { // Option<Rc<i32>>
println!("{}", *s); // 5
}
drop(strong);
assert!(weak.upgrade().is_none()); // value is gone
The upgrade() returns Some(Rc<T>) if the value is still alive, None otherwise.
The principal use is breaking reference cycles:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
let root = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Rc::downgrade(&root)), // weak reference to parent
children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(Rc::clone(&child));
If parent were Rc<Node>, the cycle would prevent both nodes from ever being dropped. The Weak<Node> breaks the cycle.
Deref and DerefMut
The Deref trait admits a smart pointer to be used like the wrapped type through automatic dereferencing:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
let b = MyBox(5);
println!("{}", *b); // 5; calls deref
The standard smart pointers (Box, Rc, Arc, String, Vec) all implement Deref; the methods of the wrapped type are accessible directly through the smart pointer:
let s = String::from("hello");
s.len(); // String::len, accessed through Deref to str
The mechanism — deref coercion — admits substantial code reuse.
Drop and resource cleanup
The Drop trait admits running cleanup when a value goes out of scope:
struct File {
handle: i32,
}
impl Drop for File {
fn drop(&mut self) {
println!("closing file {}", self.handle);
// close the OS file descriptor
}
}
{
let f = File { handle: 1 };
// ... use f ...
} // f.drop() called here
The Drop::drop is called automatically; manually calling f.drop() is forbidden by the language. To drop early, use std::mem::drop(f).
The smart pointers all implement Drop:
Box<T>::dropfrees the heap allocation.Rc<T>::dropdecrements the count; frees when count reaches zero.Arc<T>::dropdecrements atomically; frees when count reaches zero.RefCell<T>::dropreleases the runtime borrow tracking.Mutex<T>::dropreleases the lock.
The combination admits RAII-style resource management.
Common patterns
Box<dyn Trait> for dynamic dispatch
trait Drawable {
fn draw(&self);
}
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle::new(5.0)),
Box::new(Square::new(3.0)),
];
for s in &shapes {
s.draw(); // dynamic dispatch
}
The pattern admits heterogeneous collections; treated in Traits.
Rc<RefCell<T>> for single-threaded shared mutable
use std::cell::RefCell;
use std::rc::Rc;
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let view1 = Rc::clone(&shared);
let view2 = Rc::clone(&shared);
view1.borrow_mut().push(4);
println!("{:?}", view2.borrow()); // [1, 2, 3, 4]
The pattern admits multiple owners of mutable data in single-threaded contexts.
Arc<Mutex<T>> for multi-threaded shared mutable
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..10).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
*c.lock().unwrap() += 1;
})
}).collect();
for h in handles {
h.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 10
The conventional concurrent-shared-state pattern.
Lazy initialisation with OnceCell
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
fn config() -> &'static Config {
CONFIG.get_or_init(|| {
Config::load_from_file("/etc/app.conf")
})
}
OnceLock (since 1.70) admits one-time initialisation; the conventional Rust form for global lazy initialisation. Older code uses lazy_static! or once_cell::sync::Lazy.
Choice of smart pointer
| Need | Pointer |
|---|---|
| Single-owner heap allocation | Box<T> |
| Single-thread shared ownership | Rc<T> |
| Multi-thread shared ownership | Arc<T> |
| Single-thread interior mutability | RefCell<T> |
| Multi-thread interior mutability | Mutex<T> or RwLock<T> |
| Single-thread shared mutable | Rc<RefCell<T>> |
| Multi-thread shared mutable | Arc<Mutex<T>> |
| Break reference cycles | Weak<T> |
| Lazy initialisation | OnceLock<T> (or Lazy<T> from once_cell) |
| Atomic primitive | AtomicI32, etc. (from std::sync::atomic) |
The default for “I need heap allocation” is Box<T>; reach for the more elaborate forms only when shared ownership or interior mutability is genuinely needed.
A note on the cost
The smart pointers carry overhead:
Box<T>— heap allocation; one pointer indirection.Rc<T>/Arc<T>— heap allocation; pointer indirection; reference-count manipulation on clone/drop.RefCell<T>— runtime borrow tracking on borrow/borrow_mut.Mutex<T>— synchronisation overhead on lock/unlock.
The conventional Rust style minimises smart-pointer use — most code works with bare values, references, and Box for heap allocation. Rc, Arc, RefCell, Mutex are tools for specific situations, not the default.
A note on the discipline
The contemporary Rust smart-pointer advice:
- Default to plain ownership and references — most code does not need smart pointers.
- Use
Box<T>for recursive types and trait objects. - Use
Rc<T>for single-threaded shared ownership. - Use
Arc<Mutex<T>>for multi-threaded shared mutable state. - Use
Weak<T>to break cycles. - Use
OnceLock<T>for lazy globals. - Avoid stacked smart pointers (
Rc<Rc<T>>,Arc<Arc<T>>); they typically indicate a design issue.
The combination — Box, Rc, Arc, RefCell, Mutex, Weak, OnceLock — admits substantial flexibility. The conventional discipline is to choose the smallest set of pointers that admits the design and to prefer plain ownership when it is sufficient.