# Rust Syntax Smart Pointers

> 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".

- Skill: `impertio-studio/rust-syntax-smart-pointers` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/rust-syntax-smart-pointers`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/rust-syntax-smart-pointers/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/rust-syntax-smart-pointers

---


# 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

```rust
fn box_basic() {
    let b: Box<i32> = Box::new(5);
    println!("{}", *b); // Deref to i32
}
```

### When `Box` is the right tool

1. **Recursive types**. The compiler cannot size `enum List { Cons(i32, List), Nil }`; `Box` adds a fixed-size indirection.

   ```rust
   enum List {
       Cons(i32, Box<List>),
       Nil,
   }
   ```

2. **Trait objects**. `Box<dyn Error>`, `Box<dyn Fn() + Send>` give owned, dyn-dispatched values.

3. **Large stack values you want to move cheaply**. Moving a `Box<Huge>` copies one pointer; moving a `Huge` by value copies all bytes.

4. **`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.

   ```rust
   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

```rust
use std::rc::Rc;

fn rc_basic() {
    let a = Rc::new(String::from("hi"));
    let b = Rc::clone(&a);          // refcount = 2
    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).

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

struct Node {
    parent: RefCell<Weak<Node>>,    // parent: non-owning
    children: RefCell<Vec<Rc<Node>>>, // children: owning
}
```

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

```rust
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

```text
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

```rust
use std::cell::Cell;

let c = Cell::new(0_i32);
c.set(c.get() + 1);         // T: Copy fast path
```

- `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`

```rust
use std::cell::RefCell;

let r = RefCell::new(vec![1, 2, 3]);
{
    let v = r.borrow();
    println!("{}", v.len());
}                                // borrow drops
{
    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

```rust
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

```rust
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`.

```rust
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

```rust
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};

// The Future trait signature: poll receives a pinned mutable reference.
trait FutureSketch {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
```

Three facts to know in this skill:

1. **`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`.
2. **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).
3. **`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.

```rust
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

```rust
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](references/methods.md). For end-to-end examples see [examples](references/examples.md). For pitfalls see [anti-patterns](references/anti-patterns.md).

---

## Decision tree: which smart pointer

```text
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>.
```

```text
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

- API reference: [references/methods.md](references/methods.md)
- Worked examples: [references/examples.md](references/examples.md)
- Anti-patterns and why they fail: [references/anti-patterns.md](references/anti-patterns.md)
- Pinning deep dive: [[rust-syntax-async-await]]/references/pinning.md
- Borrow rules and `Cell` vs `RefCell` vs `Mutex` taxonomy: [[rust-syntax-borrowing]]
- Concurrency patterns (`Arc<Mutex<T>>`, `Arc<RwLock<T>>`, atomics): [[rust-impl-concurrency]]
- Underlying memory model and `UnsafeCell`: [[rust-core-memory-model]]
- Official: https://doc.rust-lang.org/std/boxed/index.html, https://doc.rust-lang.org/std/rc/index.html, https://doc.rust-lang.org/std/sync/index.html, https://doc.rust-lang.org/std/cell/index.html, https://doc.rust-lang.org/std/pin/index.html

