Rust’s ownership model gives you one owner per value by default — simple, safe, and predictable. But real data structures often need something more complex: trees where nodes reference their parents, graphs with shared edges, interpreter environments where closures capture mutable state. Smart pointers are the tools Rust gives you to handle these cases while preserving safety guarantees.
Box<T> — Heap Allocation with Single Ownership
Box<T> puts a value on the heap and gives you a single owning pointer to it. Use it when:
- You have a recursive type that can’t be sized at compile time
- You want to avoid copying large values
- You need a trait object (
Box<dyn Trait>)
// Recursive linked list — requires Box to break the infinite size
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
println!("{:?}", list);
// Trait object — store different types behind one interface
trait Animal {
fn sound(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn sound(&self) -> &str { "woof" } }
impl Animal for Cat { fn sound(&self) -> &str { "meow" } }
let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat), Box::new(Dog)];
for a in &animals {
println!("{}", a.sound());
}
}
Box<T> has no runtime overhead beyond the heap allocation itself — no reference counting, no locks.
Rc<T> — Shared Ownership (Single-Threaded)
Rc<T> (Reference Counted) lets multiple parts of your program share ownership of the same heap-allocated value. The value is dropped when the last Rc clone is dropped.
use std::rc::Rc;
fn main() {
let data = Rc::new(vec![1, 2, 3, 4, 5]);
let a = Rc::clone(&data); // Cheap: increments count, no deep copy
let b = Rc::clone(&data);
println!("Strong count: {}", Rc::strong_count(&data)); // 3
println!("a sum: {}", a.iter().sum::<i32>()); // 15
drop(a);
println!("After drop(a): {}", Rc::strong_count(&data)); // 2
// data and b still valid
println!("b: {:?}", b);
}
Rc is not thread-safe — it uses non-atomic operations for performance. The compiler enforces this: Rc is not Send or Sync.
Use Case: Shared Immutable Config
use std::rc::Rc;
#[derive(Debug)]
struct Config {
max_connections: u32,
timeout_ms: u64,
host: String,
}
struct ConnectionPool {
config: Rc<Config>,
}
struct RequestHandler {
config: Rc<Config>,
}
fn main() {
let config = Rc::new(Config {
max_connections: 100,
timeout_ms: 5000,
host: "localhost".to_string(),
});
let pool = ConnectionPool { config: Rc::clone(&config) };
let handler = RequestHandler { config: Rc::clone(&config) };
println!("Pool timeout: {}ms", pool.config.timeout_ms);
println!("Handler host: {}", handler.config.host);
println!("Config refs: {}", Rc::strong_count(&config)); // 3
}
RefCell<T> — Interior Mutability
Rust’s borrow checker enforces that you can’t have mutable and immutable references coexisting. RefCell<T> moves this check to runtime, letting you mutate data through a shared reference — a pattern called interior mutability:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Multiple immutable borrows at once
let r1 = data.borrow();
let r2 = data.borrow();
println!("{:?} {:?}", r1, r2); // [1, 2, 3] [1, 2, 3]
drop(r1);
drop(r2);
// Mutable borrow (only one at a time)
{
let mut w = data.borrow_mut();
w.push(4);
} // borrow_mut guard dropped here
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}
If you violate the borrow rules at runtime, RefCell panics:
let cell = RefCell::new(0);
let _r = cell.borrow();
// let _w = cell.borrow_mut(); // PANIC: already borrowed immutably
Use try_borrow() and try_borrow_mut() for non-panicking versions that return Result.
Cell<T> — For Copy Types
When T: Copy, Cell<T> is a simpler interior mutability option with no borrow guards:
use std::cell::Cell;
struct Counter {
count: Cell<u32>,
}
impl Counter {
fn new() -> Self { Counter { count: Cell::new(0) } }
fn increment(&self) { self.count.set(self.count.get() + 1); }
fn value(&self) -> u32 { self.count.get() }
}
fn main() {
let c = Counter::new();
c.increment();
c.increment();
println!("{}", c.value()); // 2
}
Rc<RefCell<T>> — Shared Mutable State (Single-Threaded)
Combining Rc and RefCell gives you multiple owners that can all mutate the shared data:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
neighbors: Vec<Rc<RefCell<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node { value, neighbors: Vec::new() }))
}
fn connect(a: &Rc<RefCell<Node>>, b: &Rc<RefCell<Node>>) {
a.borrow_mut().neighbors.push(Rc::clone(b));
b.borrow_mut().neighbors.push(Rc::clone(a));
}
}
fn main() {
let node_a = Node::new(1);
let node_b = Node::new(2);
let node_c = Node::new(3);
Node::connect(&node_a, &node_b);
Node::connect(&node_b, &node_c);
println!("Node A value: {}", node_a.borrow().value);
println!("Node A neighbors: {}",
node_a.borrow().neighbors.iter()
.map(|n| n.borrow().value.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
The GUI State Pattern
A common pattern in single-threaded GUIs and interpreters:
use std::rc::Rc;
use std::cell::RefCell;
type SharedState = Rc<RefCell<AppState>>;
#[derive(Debug)]
struct AppState {
count: u32,
messages: Vec<String>,
}
struct Button {
state: SharedState,
label: String,
}
struct Label {
state: SharedState,
}
impl Button {
fn click(&self) {
let mut s = self.state.borrow_mut();
s.count += 1;
s.messages.push(format!("'{}' clicked (count={})", self.label, s.count));
}
}
impl Label {
fn render(&self) -> String {
format!("Count: {}", self.state.borrow().count)
}
}
fn main() {
let state = Rc::new(RefCell::new(AppState {
count: 0,
messages: Vec::new(),
}));
let btn = Button { state: Rc::clone(&state), label: "Submit".to_string() };
let lbl = Label { state: Rc::clone(&state) };
btn.click();
btn.click();
println!("{}", lbl.render()); // Count: 2
for msg in &state.borrow().messages {
println!("{}", msg);
}
}
Arc<T> — Shared Ownership Across Threads
Arc<T> (Atomically Reference Counted) is the thread-safe version of Rc<T>. It uses atomic operations for the reference count, making it Send and Sync. The cost: slightly slower reference counting than Rc.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = Vec::new();
for tid in 0..4 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("Thread {}: sum = {}", tid, sum);
}));
}
for h in handles {
h.join().unwrap();
}
println!("Main: data still valid: {:?}", data);
}
Pair with Mutex or RwLock for mutable shared state across threads — see Concurrency in Rust for patterns.
Weak<T> — Non-Owning References
Weak<T> is a non-owning reference to an Rc or Arc-managed value. It does not prevent the value from being dropped. Use it to break reference cycles and express “I reference this, but I don’t own it”:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct TreeNode {
value: i32,
parent: RefCell<Weak<TreeNode>>, // Non-owning upward reference
children: RefCell<Vec<Rc<TreeNode>>>, // Owning downward references
}
impl TreeNode {
fn new(value: i32) -> Rc<Self> {
Rc::new(TreeNode {
value,
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
fn add_child(parent: &Rc<TreeNode>, child: Rc<TreeNode>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(child);
}
}
fn main() {
let root = TreeNode::new(1);
let child = TreeNode::new(2);
let leaf = TreeNode::new(3);
TreeNode::add_child(&root, Rc::clone(&child));
TreeNode::add_child(&child, Rc::clone(&leaf));
// Navigate up: upgrade() returns Option<Rc<T>>
let parent = leaf.parent.borrow().upgrade();
match parent {
Some(p) => println!("leaf's parent: {}", p.value), // 2
None => println!("no parent"),
}
println!("root strong count: {}", Rc::strong_count(&root)); // 1
println!("child strong count: {}", Rc::strong_count(&child)); // 2 (root + child var)
println!("child weak count: {}", Rc::weak_count(&child)); // 1 (leaf's parent ref)
}
Observer Pattern with Weak References
Weak is perfect for observer/event patterns where subjects shouldn’t keep observers alive:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
trait Observer {
fn on_event(&self, event: &str);
}
struct EventBus {
subscribers: Vec<Weak<dyn Observer>>,
}
impl EventBus {
fn new() -> Self { EventBus { subscribers: Vec::new() } }
fn subscribe(&mut self, obs: &Rc<dyn Observer>) {
self.subscribers.push(Rc::downgrade(obs));
}
fn publish(&mut self, event: &str) {
// Remove dead subscribers, notify live ones
self.subscribers.retain(|weak| {
if let Some(obs) = weak.upgrade() {
obs.on_event(event);
true
} else {
false // subscriber was dropped, remove it
}
});
}
}
When to Use Each Smart Pointer
| Situation | Use |
|---|---|
| Single owner, heap allocation | Box<T> |
| Trait objects | Box<dyn Trait> |
| Shared ownership, single thread | Rc<T> |
| Shared ownership, multi-thread | Arc<T> |
| Interior mutability (Copy types) | Cell<T> |
| Interior mutability (non-Copy) | RefCell<T> |
| Shared mutable state, single thread | Rc<RefCell<T>> |
| Shared mutable state, multi-thread | Arc<Mutex<T>> |
| Back-reference (break cycles) | Weak<T> |
Performance Notes
Box<T>: zero overhead beyond heap allocationRc<T>: non-atomic refcount — fast, single-thread onlyArc<T>: atomic refcount — slightly slower thanRc, thread-safeRefCell<T>: runtime borrow check — small overhead per borrowCell<T>: cheaper thanRefCellforCopytypes — no guard needed
Prefer Box<T> and plain ownership where possible. Reach for Rc/Arc only when you genuinely need shared ownership.
Summary
Smart pointers express ownership relationships that go beyond Rust’s default single-owner model:
Box<T>— owned heap value, single ownerRc<T>— shared ownership, single threadArc<T>— shared ownership, any threadRefCell<T>— interior mutability with runtime borrow checksWeak<T>— non-owning reference, breaks cycles
The combination Rc<RefCell<T>> (or Arc<Mutex<T>> for threads) covers the vast majority of advanced ownership patterns in real Rust code.
Comments