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)