Polyglot
Languages Rust smart pointers
Rust § smart-pointers

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 &self method.
  • 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>::drop frees the heap allocation.
  • Rc<T>::drop decrements the count; frees when count reaches zero.
  • Arc<T>::drop decrements atomically; frees when count reaches zero.
  • RefCell<T>::drop releases the runtime borrow tracking.
  • Mutex<T>::drop releases 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

NeedPointer
Single-owner heap allocationBox<T>
Single-thread shared ownershipRc<T>
Multi-thread shared ownershipArc<T>
Single-thread interior mutabilityRefCell<T>
Multi-thread interior mutabilityMutex<T> or RwLock<T>
Single-thread shared mutableRc<RefCell<T>>
Multi-thread shared mutableArc<Mutex<T>>
Break reference cyclesWeak<T>
Lazy initialisationOnceLock<T> (or Lazy<T> from once_cell)
Atomic primitiveAtomicI32, 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.