Skip to main content
Technology & EngineeringRust263 lines

Smart Pointers

Rust smart pointers Box, Rc, Arc, RefCell, and their combinations for heap allocation and shared ownership

Quick Summary17 lines
You are an expert in Rust smart pointers for managing heap allocation, shared ownership, and interior mutability patterns.

## Key Points

- Start with owned types and stack allocation. Reach for smart pointers only when the ownership model requires them.
- Use `Box<T>` for single ownership on the heap, recursive types, and large values you want to avoid copying on the stack.
- Use `Rc<T>` / `Arc<T>` only when multiple owners genuinely need to share data. Prefer passing references when possible.
- Prefer `Arc<RwLock<T>>` over `Arc<Mutex<T>>` for read-heavy workloads.
- Use `Weak<T>` to break reference cycles in graph-like structures.
- Use `Cow<T>` when a function sometimes needs to allocate and sometimes can borrow.
- **Reference cycles with `Rc`/`Arc`**: Two `Rc` values pointing to each other will never be freed — this is a memory leak. Use `Weak` for back-references.
- **`RefCell` panics at runtime**: Unlike compile-time borrow checking, `RefCell` panics if you violate borrowing rules. Use `try_borrow()` / `try_borrow_mut()` in uncertain situations.
- **`Mutex` poisoning**: If a thread panics while holding a `Mutex` lock, the mutex becomes "poisoned." Handle this with `.lock().unwrap_or_else(|e| e.into_inner())` if recovery is possible.
- **Using `Rc` across threads**: `Rc` is not `Send` or `Sync`. Use `Arc` for multi-threaded code. The compiler will catch this.
- **Overusing `Arc<Mutex<T>>`**: If you find yourself wrapping everything in `Arc<Mutex<T>>`, consider whether message passing (channels) would be a better architecture.
skilldb get rust-skills/Smart PointersFull skill: 263 lines
Paste into your CLAUDE.md or agent config

Smart Pointers — Rust Programming

You are an expert in Rust smart pointers for managing heap allocation, shared ownership, and interior mutability patterns.

Core Philosophy

Smart pointers exist because Rust's ownership model is deliberately restrictive -- and that restriction is the point. Single ownership with borrowing handles most cases, but some data structures (graphs, caches, shared state) genuinely need shared ownership or heap indirection. Smart pointers are the controlled escape hatches that provide these capabilities without abandoning Rust's safety guarantees.

The hierarchy of smart pointers reflects a principle of escalating cost. Box<T> adds heap allocation but keeps single ownership. Rc<T> adds reference counting but restricts you to one thread. Arc<T> adds atomic reference counting for thread safety at the cost of cache-line contention. RefCell<T> moves borrow checking to runtime, trading compile-time guarantees for flexibility. Each step up the ladder gives you more power but weaker static guarantees, so you should always start with the simplest option that works.

The combinations -- Rc<RefCell<T>> for single-threaded shared mutation, Arc<Mutex<T>> for multi-threaded shared mutation -- are not clever tricks but deliberate compositions. Rust forces you to be explicit about both sharing (Rc/Arc) and mutation (RefCell/Mutex) because conflating the two is the root cause of data races. When you see these nested types, you are reading a precise statement about the concurrency and ownership properties of that data.

Anti-Patterns

  • Defaulting to Arc<Mutex<T>> for all shared state: This is the Rust equivalent of making everything a global variable with a lock. Prefer message passing via channels, or restructure so that a single owner manages the state and others interact through requests.
  • Using Rc/Arc when a borrow would suffice: Reference counting has runtime cost (allocation, atomic operations for Arc). If the data has a clear single owner and others just need temporary access, pass &T instead of cloning an Rc.
  • Creating reference cycles with Rc or Arc: Two values holding Rc references to each other will never be freed -- Rust has no cycle collector. Use Weak<T> for back-references and parent pointers in tree/graph structures.
  • Ignoring RefCell panics in production: RefCell::borrow_mut() panics if the value is already borrowed. In tests this surfaces quickly, but in production it crashes the process. Use try_borrow_mut() when the borrowing pattern is not statically obvious, or restructure to avoid the need for runtime borrow checking.
  • Box::new for small, Copy types: Boxing an i32 or a small struct adds a heap allocation for no benefit. Use Box for recursive types, trait objects, or genuinely large values -- not as a general-purpose indirection.

Overview

Smart pointers in Rust are structs that behave like references but carry additional metadata and capabilities. They implement Deref (to act like references) and Drop (for cleanup). The core smart pointers are Box<T> (heap allocation), Rc<T> / Arc<T> (reference counting), and RefCell<T> / Mutex<T> (interior mutability).

Core Concepts

Box<T> — Heap Allocation

Box<T> allocates data on the heap with a single owner. Use it for large data, recursive types, or trait objects:

// Recursive type requires indirection
enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

// Trait object on the heap
let writer: Box<dyn std::io::Write> = Box::new(Vec::new());

Rc<T> — Single-Threaded Reference Counting

Rc<T> enables multiple owners of the same data. Not thread-safe:

use std::rc::Rc;

let shared = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&shared); // increments reference count
let clone2 = Rc::clone(&shared);

println!("ref count: {}", Rc::strong_count(&shared)); // 3
// Data is dropped when the last Rc goes out of scope

Arc<T> — Thread-Safe Reference Counting

Arc<T> is Rc<T> with atomic operations, safe to share across threads:

use std::sync::Arc;
use std::thread;

let data = Arc::new(vec![1, 2, 3]);

let handles: Vec<_> = (0..4)
    .map(|i| {
        let data = Arc::clone(&data);
        thread::spawn(move || {
            println!("thread {i}: {:?}", data);
        })
    })
    .collect();

for h in handles {
    h.join().unwrap();
}

RefCell<T> — Interior Mutability (Single-Threaded)

RefCell<T> enforces borrowing rules at runtime instead of compile time. Panics on violation:

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);

// Immutable borrow
{
    let borrowed = data.borrow();
    println!("{:?}", *borrowed);
}

// Mutable borrow
{
    let mut borrowed = data.borrow_mut();
    borrowed.push(4);
}

// PANIC at runtime: simultaneous mutable and immutable borrows
// let r1 = data.borrow();
// let r2 = data.borrow_mut(); // panics!

Cell<T> — Interior Mutability for Copy Types

Cell<T> provides interior mutability without borrowing, for Copy types:

use std::cell::Cell;

let counter = Cell::new(0u32);
counter.set(counter.get() + 1);
counter.set(counter.get() + 1);
println!("{}", counter.get()); // 2

Implementation Patterns

Rc<RefCell<T>> — Shared Mutable State (Single-Threaded)

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<RefCell<Node>>>,
}

let root = Rc::new(RefCell::new(Node {
    value: 1,
    children: vec![],
}));

let child = Rc::new(RefCell::new(Node {
    value: 2,
    children: vec![],
}));

root.borrow_mut().children.push(Rc::clone(&child));
child.borrow_mut().value = 42; // mutate through shared reference

Arc<Mutex<T>> — Shared Mutable State (Multi-Threaded)

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));

let handles: Vec<_> = (0..10)
    .map(|_| {
        let counter = Arc::clone(&counter);
        thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        })
    })
    .collect();

for h in handles {
    h.join().unwrap();
}

println!("final count: {}", *counter.lock().unwrap()); // 10

Arc<RwLock<T>> — Read-Heavy Shared State

use std::sync::{Arc, RwLock};

let config = Arc::new(RwLock::new(HashMap::new()));

// Multiple readers simultaneously
let reader = Arc::clone(&config);
let data = reader.read().unwrap();

// Single writer
let writer = Arc::clone(&config);
let mut data = writer.write().unwrap();
data.insert("key".to_string(), "value".to_string());

Weak References to Break Cycles

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

let parent = 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(&parent)),
    children: RefCell::new(vec![]),
});

parent.children.borrow_mut().push(Rc::clone(&child));

// Access parent from child
if let Some(p) = child.parent.borrow().upgrade() {
    println!("parent value: {}", p.value);
}

Cow<T> — Clone on Write

use std::borrow::Cow;

fn normalize(input: &str) -> Cow<'_, str> {
    if input.contains(' ') {
        // Only allocate when modification is needed
        Cow::Owned(input.replace(' ', "_"))
    } else {
        Cow::Borrowed(input)
    }
}

let a = normalize("hello"); // Borrowed, no allocation
let b = normalize("hello world"); // Owned, allocates

Best Practices

  • Start with owned types and stack allocation. Reach for smart pointers only when the ownership model requires them.
  • Use Box<T> for single ownership on the heap, recursive types, and large values you want to avoid copying on the stack.
  • Use Rc<T> / Arc<T> only when multiple owners genuinely need to share data. Prefer passing references when possible.
  • Prefer Arc<RwLock<T>> over Arc<Mutex<T>> for read-heavy workloads.
  • Use Weak<T> to break reference cycles in graph-like structures.
  • Use Cow<T> when a function sometimes needs to allocate and sometimes can borrow.

Common Pitfalls

  • Reference cycles with Rc/Arc: Two Rc values pointing to each other will never be freed — this is a memory leak. Use Weak for back-references.
  • RefCell panics at runtime: Unlike compile-time borrow checking, RefCell panics if you violate borrowing rules. Use try_borrow() / try_borrow_mut() in uncertain situations.
  • Mutex poisoning: If a thread panics while holding a Mutex lock, the mutex becomes "poisoned." Handle this with .lock().unwrap_or_else(|e| e.into_inner()) if recovery is possible.
  • Using Rc across threads: Rc is not Send or Sync. Use Arc for multi-threaded code. The compiler will catch this.
  • Overusing Arc<Mutex<T>>: If you find yourself wrapping everything in Arc<Mutex<T>>, consider whether message passing (channels) would be a better architecture.

Install this skill directly: skilldb add rust-skills

Get CLI access →