Smart Pointers
Choosing the Right Smart Pointer
Selecting a smart pointer or interior mutability wrapper in Rust depends on three orthogonal axes: ownership cardinality (single owner vs. shared owners), thread boundary (single-threaded vs. multi-threaded), and mutation pattern (immutable, unique mutable, or shared interior mutable).
Decision Matrix
Does the value need to live on the heap or have shared/interior-mutable access?
|
+-- Single Owner?
| +-- Fixed size & fits on stack ------------> Plain value `T` (or `&T` / `&mut T`)
| +-- Heap allocation / recursive / `dyn` ---> `Box<T>`
| +-- Clone-on-write (borrowed or owned) ----> `Cow<'a, B>`
|
+-- Shared Ownership (Single-Threaded)?
| +-- Immutable access only -----------------> `Rc<T>` (or `Rc<[T]>` / `Rc<str>`)
| +-- Mutate `Copy` / replaceable field -----> `Rc<Cell<T>>`
| +-- Mutate via `&mut T` guards ------------> `Rc<RefCell<T>>`
| +-- Non-owning back-edge (cycle breaker) --> `rc::Weak<T>`
|
+-- Shared Ownership (Multi-Threaded)?
+-- Immutable access only -----------------> `Arc<T>` (or `Arc<[T]>` / `Arc<str>`)
+-- Lock-free primitive counter/flag ------> `Arc<AtomicU64>` / `Arc<AtomicBool>`
+-- Exclusive read/write lock -------------> `Arc<Mutex<T>>`
+-- Read-heavy concurrent access ----------> `Arc<RwLock<T>>`
+-- Write-once lazy initialization --------> `OnceLock<T>` / `LazyLock<T>`
+-- Non-owning back-edge ------------------> `sync::Weak<T>`
Comprehensive Comparison Table
| Smart Pointer Combination | Owners | Thread Safe (Send + Sync) |
Access Provided | Runtime Overhead |
|---|---|---|---|---|
Box<T> |
1 |
Yes (if T: Send + Sync) |
&T, &mut T, or move *box |
Heap alloc/dealloc only |
Cow<'a, B> |
1 (borrowed or owned) |
Yes | &B, or &mut B::Owned via .to_mut() |
Tag check (discriminant); clones only on write |
Rc<T> |
$N$ | No (!Send, !Sync) |
&T (or &mut T via make_mut) |
Non-atomic counter inc/dec |
Rc<Cell<T>> |
$N$ | No | .get(), .set(), .replace() |
Non-atomic counter; zero borrow cost |
Rc<RefCell<T>> |
$N$ | No | Ref<'_, T> (&T), RefMut<'_, T> (&mut T) |
Non-atomic counter + borrow flag check |
Arc<T> |
$N$ | Yes (if T: Send + Sync) |
&T (or &mut T via make_mut) |
Atomic counter inc/dec |
Arc<Mutex<T>> |
$N$ | Yes (if T: Send) |
MutexGuard<'_, T> (&mut T) |
Atomic counter + OS/futex lock |
Arc<RwLock<T>> |
$N$ | Yes (if T: Send + Sync) |
RwLockReadGuard (&T) or RwLockWriteGuard (&mut T) |
Atomic counter + reader-writer lock |
Clone-on-Write with std::borrow::Cow<'a, B>
When a function usually inspects borrowed data (&'a str or &'a [T]) without modifying it, and only occasionally allocates a modified copy, Cow<'a, B> (Cow::Borrowed(&'a B) vs. Cow::Owned(<B as ToOwned>::Owned)) avoids heap allocation on the fast path while still acting like a smart pointer via Deref<Target = B>.
use std::borrow::Cow;
fn sanitize_identifier(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
fn main() {
let clean = sanitize_identifier("rust_2024");
let dirty = sanitize_identifier("hello world");
println!(
"clean: {:?} (borrowed = {})",
clean,
matches!(clean, Cow::Borrowed(_))
);
println!(
"dirty: {:?} (borrowed = {})",
dirty,
matches!(dirty, Cow::Borrowed(_))
);
}
clean: "rust_2024" (borrowed = true)
dirty: "hello_world" (borrowed = false)