| name | rust-syntax-smart-pointers |
| description | Use when the user picks Box / Rc / Arc / RefCell / Cell / OnceCell / OnceLock / LazyLock / Weak / Pin, asks about reference counting, runtime-checked borrows, lazy globals, or breaking reference cycles. Prevents Arc<Mutex<T>> when atomic suffices, Rc across threads, holding a RefCell guard across calls, or static-mut lazy globals where OnceLock fits. Covers: Box (heap, recursive types, trait objects, leak), Rc (single-thread refcount, Weak), Arc (thread-safe refcount), RefCell (runtime borrow check), Cell (Copy interior), OnceCell, OnceLock (1.70), LazyLock (1.80), Weak<T> for cycle-breaking, Pin overview (deep ref in rust-syntax-async-await/references/pinning.md). Keywords: Box, Rc, Arc, RefCell, Cell, OnceCell, OnceLock, LazyLock, Weak, Pin, "smart pointer", "heap allocation", "reference counting", "lazy global", "cycle", "ref cycle", "interior mutability", "Rc<RefCell<T>>", "Arc<Mutex<T>>", "Arc<RwLock<T>>", "thread-safe", "Send Sync Rc", "what is Pin", "single thread", "multi thread".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
rust-syntax-smart-pointers
Owning pointers and shared-ownership primitives: Box<T>, Rc<T>, Arc<T>, RefCell<T>, Cell<T>, OnceCell<T>, OnceLock<T>, LazyLock<T>, Weak<T>, and a Pin<T> overview. Each picks a different combination of: heap allocation, single-vs-multi-thread refcounting, interior mutability, and lazy initialization. Choosing wrongly leaks, deadlocks, panics, or fails to compile.
Cross-references: [[rust-core-memory-model]] (stack vs heap, UnsafeCell), [[rust-syntax-borrowing]] (the borrow rules RefCell enforces at runtime), [[rust-syntax-async-await]] (Pin<&mut Self> in Future::poll; deep pinning reference under that skill), [[rust-impl-concurrency]] (Arc<Mutex<T>> vs atomics in real workloads).
When to use this skill
- User reaches for
Box<T> for a recursive type (tree, linked list) or a dyn Trait return.
- User wants to share read-only data across a single thread (
Rc<T>) or across threads (Arc<T>).
- User asks "how do I mutate through a shared reference" and needs to choose
Cell / RefCell / Mutex / RwLock / Atomic*.
- User wants a lazily-initialized global and is tempted to use
static mut or lazy_static!.
- User builds a graph / observer / parent-child tree and hits a reference cycle (memory leak with
Rc<RefCell<T>>).
- User sees
Future::poll(self: Pin<&mut Self>, ...) and asks "what is Pin".
The decision in one table
| You need... | Pick | Why |
|---|
| Heap allocation of a single owned value | Box<T> | Cheapest owned heap pointer. |
Recursive type (Node { next: ??? }) | Box<Node> | Breaks the infinite size. |
| Owned trait object | Box<dyn Trait> | Heap-stored vtable pointer. |
| Shared ownership, single thread | Rc<T> | Non-atomic refcount, no Send/Sync cost. |
| Shared ownership, multi thread | Arc<T> | Atomic refcount. T must be Send + Sync for Arc<T>: Send + Sync. |
| Shared + Copy interior | Cell<T> (single thread) / Atomic* (multi thread) | Zero or near-zero cost. |
| Shared + runtime-checked borrow | RefCell<T> (single thread) / Mutex<T>/RwLock<T> (multi thread) | Panic / block on violation. |
| Init-once value | OnceCell<T> (single thread) / OnceLock<T> (multi thread, 1.70+) | No lock after init. |
| Lazy global (init on first access) | LazyLock<T> (1.80+) static; LazyCell<T> (1.80+) thread-local | Replaces lazy_static!. |
| Cycle-breaking child pointer | Weak<T> (from Rc::downgrade / Arc::downgrade) | Does not count toward strong refs. |
| Self-referential struct (async future state) | Pin<P> | See Pin overview below and [[rust-syntax-async-await]]. |
ALWAYS pick the lowest-overhead primitive that satisfies the requirement. NEVER reach for Arc<Mutex<T>> as a first instinct: in many cases AtomicUsize, RwLock, or Arc<T> alone is correct.
Box<T>: owned heap allocation
Box<T> is the simplest smart pointer: it owns a single T on the heap. Drops the value when the Box drops.
Source: https://doc.rust-lang.org/std/boxed/index.html
fn box_basic() {
let b: Box<i32> = Box::new(5);
println!("{}", *b);
}
When Box is the right tool
-
Recursive types. The compiler cannot size enum List { Cons(i32, List), Nil }; Box adds a fixed-size indirection.
enum List {
Cons(i32, Box<List>),
Nil,
}
-
Trait objects. Box<dyn Error>, Box<dyn Fn() + Send> give owned, dyn-dispatched values.
-
Large stack values you want to move cheaply. Moving a Box<Huge> copies one pointer; moving a Huge by value copies all bytes.
-
Box::leak for 'static references. Promotes the heap allocation to live for the program's lifetime, returning &'static mut T. Used for one-time-initialized configuration that the rest of the program borrows.
let cfg: &'static str = Box::leak(Box::new(String::from("hi"))).as_str();
NEVER call Box::leak in a hot path or per-request. The memory is genuinely leaked.
NEVER Box<T> a small Copy type that fits in a register (Box<u32>, Box<bool>). The heap round-trip is pure cost with no benefit.
Rc<T>: single-thread shared ownership
Rc<T> ("reference counted") gives shared, immutable access to a T on the heap. Cloning increments a non-atomic counter. The last Rc drops the value.
Source: https://doc.rust-lang.org/std/rc/index.html
use std::rc::Rc;
fn rc_basic() {
let a = Rc::new(String::from("hi"));
let b = Rc::clone(&a);
println!("{} {} count={}", a, b, Rc::strong_count(&a));
}
Rc<T>: !Send and Rc<T>: !Sync. Compiler error on cross-thread move.
Rc::clone(&a) is preferred over a.clone() to make the intent explicit; both do the same thing.
Rc<T> is read-only. Combine with RefCell<T> for shared+mutable single-thread: Rc<RefCell<T>>.
ALWAYS use Rc::clone(&a), not a.clone(), to communicate "I am bumping a refcount, not deep-copying".
NEVER hold an Rc<T> in a struct that will be sent to another thread. Switch to Arc<T> from the start; retrofitting is invasive.
Weak<T>: break cycles
Rc<RefCell<Node>> graphs can leak when two nodes point at each other (parent <-> child). Weak<T> is a non-owning pointer: it does not contribute to the strong count and must be upgrade()d to access the value (returns Option<Rc<T>> because the value may have been dropped).
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
ALWAYS make the back-edge (parent, observer-of) Weak. The forward-edge (child, owner) stays Rc/Arc. This is the canonical fix for cycles.
NEVER make BOTH directions Rc/Arc. Drop order will never run; memory leaks silently.
Arc<T>: multi-thread shared ownership
Arc<T> ("atomically reference counted") is the multi-thread analogue of Rc<T>. Cloning uses atomic increment.
Source: https://doc.rust-lang.org/std/sync/struct.Arc.html
use std::sync::Arc;
use std::thread;
fn arc_basic() {
let data = Arc::new(vec![1, 2, 3]);
let h: Vec<_> = (0..3).map(|_| {
let d = Arc::clone(&data);
thread::spawn(move || println!("{:?}", d))
}).collect();
for j in h { j.join().unwrap(); }
}
Arc<T>: Send + Sync iff T: Send + Sync. The compiler enforces this.
- Atomic operations are more expensive than
Rc's non-atomic ones, but still cheap (typically a single CAS).
- For mutation across threads, wrap in
Mutex, RwLock, or an atomic type: Arc<Mutex<T>>, Arc<RwLock<T>>, Arc<AtomicUsize>.
ALWAYS reach for Arc<T> only when you actually need cross-thread shared ownership. If only one thread reads, Rc<T> is faster.
NEVER use Arc<Mutex<T>> when an atomic suffices. For counters, flags, and primitive ints/bools, Arc<AtomicUsize> (or even a plain &'static AtomicUsize) is faster and harder to misuse.
Arc<Mutex<T>> vs Arc<RwLock<T>> vs atomics
Mutates a single integer/bool/pointer?
|
+-- Yes: AtomicX (AtomicUsize, AtomicBool, etc.). No Arc<Mutex> needed for primitives.
|
+-- No: many readers, occasional writer?
|
+-- Yes: Arc<RwLock<T>>. Readers parallel, writers exclusive.
|
+-- No (mixed read/write, contention low): Arc<Mutex<T>>. Simplest.
ALWAYS measure before assuming Arc<RwLock<T>> is faster than Arc<Mutex<T>>. RwLock is more complex and not always a win under contention.
NEVER hold a std::sync::Mutex guard across .await. The runtime can move the task to another thread. Use tokio::sync::Mutex in async code. See [[rust-impl-concurrency]] and [[rust-syntax-async-await]].
Cell<T> and RefCell<T>: interior mutability (single thread)
The borrow rules ([[rust-syntax-borrowing]]) say: many &T OR one &mut T, never both. Sometimes you genuinely need to mutate through &T. Cell and RefCell are the sound escape hatches, both built on UnsafeCell<T>.
Cell<T>: zero-cost, get/set only
use std::cell::Cell;
let c = Cell::new(0_i32);
c.set(c.get() + 1);
Cell<T> never hands out &T or &mut T to the contents. Only get() (returns T by copy) and set(T) (replace).
- For
T: !Copy, use replace(new), take(), swap(other).
!Sync (single thread only).
- Zero runtime overhead.
ALWAYS prefer Cell<T> over RefCell<T> when T: Copy or you only need swap semantics.
NEVER try to read &T out of a Cell. The API does not allow it by design.
RefCell<T>: runtime-checked & / &mut
use std::cell::RefCell;
let r = RefCell::new(vec![1, 2, 3]);
{
let v = r.borrow();
println!("{}", v.len());
}
{
let mut v = r.borrow_mut();
v.push(4);
}
borrow() returns Ref<'_, T>, borrow_mut() returns RefMut<'_, T>. Both are smart pointers that release the borrow on drop.
- Violating the borrow rules at runtime panics: "already borrowed" / "already mutably borrowed".
RefCell<T>: !Sync.
- Use
try_borrow() / try_borrow_mut() to handle the failure without panic in re-entrant code.
ALWAYS limit the scope of Ref / RefMut to the smallest possible block. The borrow lives until the guard drops, exactly like MutexGuard.
NEVER hold a RefCell guard across a function call that might re-enter the same cell. Runtime panic.
NEVER call borrow_mut() while a borrow() from the same cell is alive in any code path. Same panic.
OnceCell / OnceLock / LazyLock: lazy initialization
For "initialize this exactly once, then read it forever" patterns. Pre-1.70 / pre-1.80 code used lazy_static! or once_cell crate. Std now ships these natively.
OnceCell<T> (single thread)
Source: https://doc.rust-lang.org/std/cell/struct.OnceCell.html
use std::cell::OnceCell;
let cell: OnceCell<String> = OnceCell::new();
let value: &String = cell.get_or_init(|| "hi".to_string());
!Sync.
get_or_init(|| ...) runs the closure exactly once (on first call).
OnceLock<T> (multi thread, stable in 1.70)
Source: https://doc.rust-lang.org/std/sync/struct.OnceLock.html
use std::sync::OnceLock;
static CFG: OnceLock<String> = OnceLock::new();
fn config() -> &'static String {
CFG.get_or_init(|| std::env::var("CFG").unwrap_or_default())
}
Sync (safe to use as static).
- After init, reads are lock-free.
- Replaces
lazy_static! for one-time initialization.
ALWAYS use OnceLock<T> for thread-safe lazy static initialization on Rust 1.70+.
NEVER use static mut for lazy globals. Reading static mut in safe code is forbidden in edition 2024 (static_mut_refs is hard error). OnceLock is the safe replacement.
LazyLock<T> (multi thread, stable in 1.80)
Source: https://doc.rust-lang.org/std/sync/struct.LazyLock.html
LazyLock<T, F> wraps an init closure into the static itself, so the call site looks like a plain &T.
use std::sync::LazyLock;
static GREETING: LazyLock<String> = LazyLock::new(|| "hello".to_string());
fn main() {
println!("{}", *GREETING);
}
- Deref to
&T. No get_or_init boilerplate at call sites.
- Single-thread variant is
LazyCell<T, F> (also stable in 1.80).
- Internally a
OnceLock plus the init closure.
ALWAYS prefer LazyLock<T> over OnceLock<T> when the call sites should look like a plain &T with no get_or_init. Use OnceLock<T> when initialization depends on runtime input not available at definition site.
NEVER use the external lazy_static! or once_cell crate in new code on Rust 1.80+. The std types replace them with zero ceremony.
Pin<P>: pinning overview (deep reference under async-await)
Pin<P> is a wrapper around a pointer type P (typically Pin<Box<T>> or Pin<&mut T>) that guarantees the pointee will not be moved in memory for the rest of its lifetime. Required to make self-referential types sound, which is why Future::poll takes self: Pin<&mut Self>.
Source: https://doc.rust-lang.org/std/pin/index.html
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};
trait FutureSketch {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Three facts to know in this skill:
Unpin opts a type out of the pinning restriction. Almost all everyday types are Unpin (the trait is an auto-trait). For an Unpin type, Pin<Box<T>> behaves identically to Box<T> and Pin<&mut T> behaves identically to &mut T.
- The drop guarantee. Once a value is pinned, its drop handler MUST be called before its memory is invalidated. Leaking via
mem::forget is allowed (it skips drop but never invalidates memory).
PhantomPinned opts a type IN to the pinning restriction. Use this in types that store self-references (typically generated by async fn, or by hand in custom futures).
ALWAYS treat Pin<P> as a marker that says "this pointer's target cannot be moved". When writing your own futures, see the deep reference: [[rust-syntax-async-await]]/references/pinning.md.
NEVER attempt to extract a &mut T out of a Pin<&mut T> for T: !Unpin via Pin::into_inner_unchecked: that breaks the pin guarantee and is unsound. The safe path is Pin::as_mut and pin-projection (see deep reference).
Common compositions
Rc<RefCell<T>>: shared + mutable, single thread
The canonical pattern for graphs, observer registries, interpreters, ECS prototypes in single-threaded code.
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(0_i32));
let copy = Rc::clone(&shared);
*shared.borrow_mut() += 1;
*copy.borrow_mut() += 10;
assert_eq!(*shared.borrow(), 11);
ALWAYS keep RefCell::borrow_mut() scopes as small as possible. A panic from "already borrowed" is the most common bug with this pattern.
Arc<Mutex<T>>: shared + mutable, multi thread
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0_u32));
let handles: Vec<_> = (0..4).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
let mut g = c.lock().unwrap();
*g += 1;
})
}).collect();
for h in handles { h.join().unwrap(); }
assert_eq!(*counter.lock().unwrap(), 4);
ALWAYS hold MutexGuard for the shortest critical section possible. Drop before any unrelated work.
NEVER hold a std::sync::Mutex guard across .await. Use tokio::sync::Mutex in async code.
Quick reference table
| Type | Send | Sync | Cost | Use when |
|---|
Box<T> | iff T: Send | iff T: Sync | one heap alloc | heap-owned single value, recursive types, trait objects |
Rc<T> | NO | NO | non-atomic refcount | shared read-only, single thread |
Weak<T> (rc) | NO | NO | non-atomic | break Rc cycles |
Arc<T> | iff T: Send + Sync | iff T: Send + Sync | atomic refcount | shared read-only, multi thread |
Weak<T> (sync) | iff T: Send + Sync | iff T: Send + Sync | atomic | break Arc cycles |
Cell<T> | iff T: Send | NO | zero | Copy interior, single thread |
RefCell<T> | iff T: Send | NO | runtime borrow check | &/&mut through &Self, single thread |
OnceCell<T> | iff T: Send | NO | one-time init | lazy single-thread |
OnceLock<T> | iff T: Send + Sync | iff T: Send + Sync | one CAS + atomic load | lazy multi-thread, since 1.70 |
LazyLock<T, F> | iff T, F: Send | iff T: Send + Sync, F: Send | one-time init + atomic load | lazy multi-thread static, since 1.80 |
Pin<P> | depends on P | depends on P | none | self-referential types, futures |
For full method signatures see methods. For end-to-end examples see examples. For pitfalls see anti-patterns.
Decision tree: which smart pointer
Need shared ownership?
|
+-- No: do you need heap allocation?
| |
| +-- Yes (recursive type, dyn Trait, large value): Box<T>
| |
| +-- No (small Copy type): just T on the stack
|
+-- Yes: single thread or multi thread?
|
+-- Single thread: Rc<T> (+ RefCell<T> if mutation needed)
| |
| +-- Need to break a cycle? Weak<T> for the back-edge.
|
+-- Multi thread: Arc<T> (+ Mutex/RwLock/Atomic if mutation needed)
|
+-- Mutating a primitive int/bool/pointer? AtomicX, no Mutex.
+-- Many readers, occasional writer? RwLock.
+-- Otherwise: Mutex.
+-- Need to break a cycle? sync::Weak<T>.
Need a lazily-initialized value?
|
+-- Single thread, manual get_or_init: OnceCell<T>
+-- Single thread, deref-style global: LazyCell<T, F> (1.80+)
+-- Multi thread, manual get_or_init: OnceLock<T> (1.70+)
+-- Multi thread, deref-style static: LazyLock<T, F> (1.80+)
ALWAYS follow the tree top to bottom. Picking the wrong primitive shows up as performance, deadlock, or compile-error months later.
Reference links