Smart Pointers
Smart pointers manage ownership and sharing of heap-allocated data with automatic cleanup.
Overview
| Type | Thread-safe | Use Case | Energy Cost |
|---|---|---|---|
Box<T> | N/A | Heap allocation, recursive types | Allocation only |
Rc<T> | No | Single-threaded shared ownership | 3.0 pJ clone/drop |
Arc<T> | Yes | Multi-threaded shared ownership | 3.0 pJ clone/drop (atomic) |
Cow<T> | N/A | Clone-on-write optimization | Free reads, allocation on write |
Box<T>
Heap-allocated value. Required for recursive types. Box<T> is a pointer in memory — zero overhead beyond the allocation.
// Recursive type requires Box
pub enum Expr {
Literal(i32),
Add { left: Box<Expr>, right: Box<Expr> },
Neg { inner: Box<Expr> },
}
let expr = Expr::Add {
left: Box::new(Expr::Literal(1)),
right: Box::new(Expr::Literal(2)),
};
Methods
let b = Box::new(42);
let inner = b.into_inner(); // 42 — consumes the Box
let r: &i32 = b.as_ref(); // borrow the inner value
let r: &mut i32 = b.as_mut(); // mutable borrow
let ptr = b.leak(); // leak memory, return raw pointer
Rc<T>
Reference-counted pointer for single-threaded shared ownership. Multiple Rc<T> values can point to the same data. The data is freed when the last Rc is dropped.
let a = Rc::new(42);
let b = a.clone(); // increment reference count (3.0 pJ)
let c = a.clone(); // count is now 3
println!("{}", Rc::strong_count(&a)); // 3
// When a, b, c all go out of scope, the value is freed
Methods
let rc = Rc::new(vec![1, 2, 3]);
let count = Rc::strong_count(&rc); // number of references
let inner = Rc::into_inner(rc); // unwrap if count == 1
let r: &Vec<i32> = rc.as_ref(); // borrow inner value
// Mutable access (only if count == 1)
let mut rc = Rc::new(42);
if let Option::Some(val) = Rc::get_mut(&mut rc) {
*val = 100;
}
Use Case: Shared Graph Nodes
pub struct Node {
pub value: i32,
pub children: Vec<Rc<Node>>,
}
let leaf = Rc::new(Node { value: 1, children: Vec::new() });
let parent = Node {
value: 0,
children: vec![leaf.clone(), leaf.clone()], // shared ownership
};
Arc<T>
Atomically reference-counted pointer for multi-threaded shared ownership. Same API as Rc<T>, but uses atomic operations for thread safety.
use std::concurrency::spawn;
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let handle = spawn(|| {
let local = data.clone(); // atomic increment (3.0 pJ)
println!("len = {}", local.len());
});
println!("len = {}", data.len()); // still valid in main thread
Methods
let arc = Arc::new(42);
let count = Arc::strong_count(&arc); // number of references
let r: &i32 = arc.as_ref(); // borrow inner value
let cloned = arc.clone(); // atomic increment
// Arc::get_mut — only if count == 1
// Arc::into_inner — unwrap if count == 1
// Arc::make_mut — clone inner if shared, then return &mut
Energy: Rc vs Arc
| Operation | Rc | Arc |
|---|---|---|
clone | 3.0 pJ (increment) | 3.0 pJ (atomic increment) |
drop | 3.0 pJ (decrement + conditional free) | 3.0 pJ (atomic decrement + conditional free) |
as_ref | 0 pJ (pointer deref) | 0 pJ (pointer deref) |
Use Rc when data stays on one thread. Use Arc when sharing across threads. The energy cost is similar, but Arc incurs cache-line contention overhead under high concurrency.
Cow<T>
Clone-on-write smart pointer. Wraps either a borrowed reference or an owned value. Reading is free; writing clones the data only if it's currently borrowed.
// Start with a borrowed value
let text = Cow::borrowed("hello");
println!("{}", text.as_ref()); // free — no allocation
// Convert to owned only when needed
let owned = text.to_owned(); // allocates if borrowed
// Check state
text.is_borrowed(); // true
text.is_owned(); // false
Methods
let cow = Cow::borrowed("hello");
let cow2 = Cow::owned("world".to_string());
let r: &str = cow.as_ref(); // borrow — always free
let s: String = cow.into_owned(); // consume, clone if borrowed
let owned = cow.to_owned(); // clone if borrowed, return owned Cow
cow.is_borrowed(); // true if wrapping a reference
cow.is_owned(); // true if wrapping an owned value
Use Case: Conditional Transformation
fn normalize(input: &str) -> Cow<str> {
if input.contains(' ') {
// Only allocate when we actually need to modify
Cow::owned(input.replace(' ', "_"))
} else {
// No allocation — return a reference to the original
Cow::borrowed(input)
}
}
// Most inputs pass through without allocation
let a = normalize("hello"); // Cow::borrowed — 0 allocation
let b = normalize("hello world"); // Cow::owned — 1 allocation
Choosing a Smart Pointer
- Need heap allocation for recursive types? Use
Box<T> - Need shared ownership on one thread? Use
Rc<T> - Need shared ownership across threads? Use
Arc<T> - Need to avoid cloning until mutation? Use
Cow<T> - Need unique ownership? Just use the value directly (no pointer needed)