Smart Pointers
Rust smart pointers Box, Rc, Arc, RefCell, and their combinations for heap allocation and shared ownership
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 linesSmart 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/Arcwhen 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&Tinstead of cloning anRc. - Creating reference cycles with
RcorArc: Two values holdingRcreferences to each other will never be freed -- Rust has no cycle collector. UseWeak<T>for back-references and parent pointers in tree/graph structures. - Ignoring
RefCellpanics in production:RefCell::borrow_mut()panics if the value is already borrowed. In tests this surfaces quickly, but in production it crashes the process. Usetry_borrow_mut()when the borrowing pattern is not statically obvious, or restructure to avoid the need for runtime borrow checking. Box::newfor small,Copytypes: Boxing ani32or a small struct adds a heap allocation for no benefit. UseBoxfor 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>>overArc<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: TwoRcvalues pointing to each other will never be freed — this is a memory leak. UseWeakfor back-references. RefCellpanics at runtime: Unlike compile-time borrow checking,RefCellpanics if you violate borrowing rules. Usetry_borrow()/try_borrow_mut()in uncertain situations.Mutexpoisoning: If a thread panics while holding aMutexlock, the mutex becomes "poisoned." Handle this with.lock().unwrap_or_else(|e| e.into_inner())if recovery is possible.- Using
Rcacross threads:Rcis notSendorSync. UseArcfor multi-threaded code. The compiler will catch this. - Overusing
Arc<Mutex<T>>: If you find yourself wrapping everything inArc<Mutex<T>>, consider whether message passing (channels) would be a better architecture.
Install this skill directly: skilldb add rust-skills
Related Skills
Async Rust
Async Rust programming with async/await, Tokio runtime, futures, and concurrent task patterns
Cargo Workspace
Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization
Error Handling
Rust error handling with Result, Option, the ? operator, and ecosystem crates anyhow and thiserror
Lifetimes
Rust lifetime annotations for ensuring reference validity and understanding the borrow checker
Ownership Borrowing
Rust ownership, borrowing, and move semantics for writing memory-safe code without a garbage collector
Pattern Matching
Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns