Interior-Mutability

154. UnsafeCell<T> — The Primitive Every Interior-Mutability Type Is Built On

This morning’s OnceCell / LazyCell bite — and Cell, RefCell, Mutex, RwLock, OnceLock, every atomic — all bottom out at the same type: UnsafeCell<T>. It is the one and only legal way in Rust to mutate through a shared reference. You’ll almost never type its name, but knowing what it does explains why all the rest exist.

The pain: &T is “you may not mutate” — to the optimizer too

Outside UnsafeCell, the compiler is allowed to assume &T points to data that won’t change underneath it. It can cache loads, hoist reads out of loops, mark pointers noalias in LLVM. Try to fake interior mutability with a raw cast and you’ve signed up for undefined behavior — not “it works but is ugly,” actual UB the optimizer is free to exploit:

1
2
3
4
fn cheat(r: &u32) {
    let p = r as *const u32 as *mut u32;
    unsafe { *p = 99; } // UB: mutating through &T without UnsafeCell
}

The cast compiles. The write may even appear to happen. But the program has lost all guarantees, and the next release of rustc — or a different opt level — can break it.

The fix: UnsafeCell::get returns *mut T through &self

UnsafeCell<T> is the one type the compiler treats specially: a &UnsafeCell<T> is not a promise that the inside won’t change, so noalias and friends don’t apply. Its .get() method takes &self and hands back a raw *mut T:

1
2
3
4
5
6
use std::cell::UnsafeCell;

let cell = UnsafeCell::new(10u32);
let p: *mut u32 = cell.get();    // legal through &cell
unsafe { *p = 20; }              // legal — UnsafeCell opted out of the no-mutation rule
assert_eq!(unsafe { *cell.get() }, 20);

That’s the whole feature. Every other interior-mutability type in std is a safe wrapper around exactly this primitive plus an invariant — a borrow counter, a lock bit, an atomic flag — that justifies the unsafe block inside.

Build Cell from scratch in 20 lines

To see it in action, here’s a stripped-down Cell<T> clone. Real std::cell::Cell is more polished, but the shape is identical:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::cell::UnsafeCell;

struct MyCell<T> {
    value: UnsafeCell<T>,
}

impl<T: Copy> MyCell<T> {
    fn new(v: T) -> Self {
        Self { value: UnsafeCell::new(v) }
    }

    fn get(&self) -> T {
        // SAFETY: UnsafeCell makes Self !Sync, so no thread races us.
        // We never hand out a reference into the cell, so no aliased &mut exists.
        unsafe { *self.value.get() }
    }

    fn set(&self, v: T) {
        // SAFETY: same as `get`.
        unsafe { *self.value.get() = v; }
    }
}

let counter = MyCell::new(0u32);
counter.set(counter.get() + 1);
counter.set(counter.get() + 1);
assert_eq!(counter.get(), 2);

UnsafeCell does the optimizer-level work; the Copy bound plus “never lend a reference out” does the safety work. Drop either piece and the type is unsound.

The !Sync default — and how Mutex opts back in

UnsafeCell<T> is !Sync even when T: Sync. That’s deliberate: if you could share an &UnsafeCell<T> across threads with no synchronization, two threads could race on the inside and you’d be back to UB.

That’s why Cell and RefCell are !Sync (single-thread only), and why Mutex<T> carries an explicit:

1
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

The unsafe impl is the author asserting “I added the lock; now sharing across threads is sound.” The pattern recurs in every sync interior-mutability type — RwLock (tomorrow’s afternoon bite), OnceLock, LazyLock, the atomics. The shape is always: UnsafeCell<T> + an invariant + an unsafe impl Sync.

When to reach for it yourself

In application code: basically never. Cell, RefCell, Mutex, RwLock, OnceCell, OnceLock cover every common pattern and they’re already audited.

Real reasons to hold UnsafeCell directly: writing a new lock primitive, an arena that hands out &mut slots from &self, a lock-free data structure, FFI cells that mirror an existing C struct. If you’re not building infrastructure of that kind, the right move is to use a wrapper that already wraps it — and now you know what’s in the bottom of the box.

153. OnceCell<T> — Memoize Through &self Without Wrapping in RefCell

You have a parse(&self) -> &Heavy accessor that needs to compute once and cache. &self rules out a plain field assignment. Cell needs Copy. RefCell won’t lend the inside out past .borrow(). OnceCell<T> is the missing piece — write-once, &self API, hands back a real &T that lives as long as the cell.

The pain: &self memoization is awkward

Classic shape — an immutable-looking accessor that’s expensive on first call and free afterwards:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::RefCell;

struct DocSlow {
    raw: String,
    parsed: RefCell<Option<Vec<String>>>,
}

impl DocSlow {
    fn lines(&self) -> Vec<String> {
        let mut slot = self.parsed.borrow_mut();
        if slot.is_none() {
            *slot = Some(self.raw.lines().map(str::to_owned).collect());
        }
        slot.clone().unwrap() // can't return a borrow that escapes RefMut
    }
}

Two problems. We .clone() on every call because a Ref<'_, T> can’t outlive the borrow() it came from. And Option<Vec<String>> plus runtime borrow checking is overkill for “set this exactly once.”

The fix: OnceCell::get_or_init

OnceCell<T> stores at most one value. get_or_init runs the closure the first time it’s called and returns &T ever after — and that &T is tied to the lifetime of &self, so you can hand it back without cloning:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::cell::OnceCell;

struct Doc {
    raw: String,
    parsed: OnceCell<Vec<String>>,
}

impl Doc {
    fn new(s: &str) -> Self {
        Self { raw: s.to_owned(), parsed: OnceCell::new() }
    }

    fn lines(&self) -> &[String] {
        self.parsed
            .get_or_init(|| self.raw.lines().map(str::to_owned).collect())
    }
}

let doc = Doc::new("one\ntwo\nthree");
assert_eq!(doc.lines(), &["one", "two", "three"]);
assert_eq!(doc.lines().len(), 3); // cached — closure does not run again

No Option, no clone, no borrow_mut. The closure fires exactly once even across multiple calls, and the returned slice is good for as long as the &Doc is.

When you want to decide later, not on first read

OnceCell doesn’t require a closure at the call site. Use set when initialization is driven by something outside the cell — a parsed CLI flag, a value computed by a sibling method, anything that doesn’t fit a self-contained || ...:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::cell::OnceCell;

let cell: OnceCell<String> = OnceCell::new();
assert_eq!(cell.get(), None);

cell.set("loaded".into()).unwrap();
assert_eq!(cell.get(), Some(&"loaded".to_string()));

// Second set is rejected — the cell is full.
assert!(cell.set("nope".into()).is_err());

set returns Err(value) on the second call so you get your input back instead of dropping it on the floor. Reach for set when initialization is driven from outside; reach for get_or_init when it isn’t.

LazyCell<T, F>: when the closure is fixed at construction

If you already know how to build the value when you create the cell, LazyCell bakes the closure in and skips the Option-style API. The first deref runs it:

1
2
3
4
5
6
7
8
use std::cell::LazyCell;

let tags: LazyCell<Vec<&'static str>> = LazyCell::new(|| {
    vec!["rust", "interior-mutability"]
});

assert_eq!(tags.len(), 2);   // closure runs here
assert_eq!(tags[0], "rust"); // cached

Rule of thumb: LazyCell when there is exactly one obvious way to compute the value and you want the cell to handle it; OnceCell when you need set from outside, or different get_or_init closures at different call sites.

Thread safety

Both types are !Sync — they’re the single-thread counterparts to OnceLock / LazyLock. If a static or a field shared across threads needs this pattern, swap to the sync versions. The API shape is intentionally the same; only the guarantees (and the cost) change.

152. RefCell<T> — When You Need to Actually Borrow the Inside

This morning’s Cell<T> only lets you swap whole values in and out. The moment you want to call .push() on the Vec inside, or hand out a &str slice of the String inside, you need a real &mut — and that’s exactly what RefCell<T> gives you, just with the aliasing rules checked at runtime instead of compile time.

The pain: Cell can’t lend the inside out

Cell<T> is great until you need to do anything to the value in place:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::cell::Cell;

let log: Cell<Vec<String>> = Cell::new(Vec::new());

// We want: log.push("hit".into());
// We can't — Cell only gives us `get` (copy) and `set` (overwrite).
// `log.get_mut()` exists, but that needs `&mut Cell`, defeating the point.

// Workaround: take the Vec out, mutate, put it back.
let mut v = log.take();
v.push("hit".into());
log.set(v);

The take/mutate/replace dance works but it’s noisy, and any code that runs between take and set sees an empty Vec. For non-trivial data structures — a cache, an arena, a graph node — that “hole” is unworkable.

The fix: borrow() and borrow_mut()

RefCell<T> hands out actual references through &self. borrow() gives you a Ref<T> (deref to &T), borrow_mut() gives you a RefMut<T> (deref to &mut T):

1
2
3
4
5
6
7
8
9
use std::cell::RefCell;

let log: RefCell<Vec<String>> = RefCell::new(Vec::new());

log.borrow_mut().push("first".into());
log.borrow_mut().push("second".into());

assert_eq!(log.borrow().len(), 2);
assert_eq!(log.borrow()[0], "first");

Both methods take &self, so this works behind an Rc, inside an Fn closure, in any field of a struct you only have a shared reference to. That’s the whole point of interior mutability — &self outside, &mut inside.

The invariant: same rules, just checked at runtime

RefCell enforces the exact same aliasing rule the compiler enforces statically: many &T xor one &mut T. Try to break it and it panics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::RefCell;

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

let r1 = cell.borrow();
let r2 = cell.borrow();    // fine — multiple readers
assert_eq!(r1.len(), 3);
assert_eq!(r2.len(), 3);
drop((r1, r2));

let mut w = cell.borrow_mut();
w.push(4);
// cell.borrow();          // would panic: already mutably borrowed
drop(w);

assert_eq!(cell.borrow().len(), 4);

The panic message is already borrowed: BorrowMutError (or BorrowError). If you can’t guarantee statically that the borrows are well-nested, use try_borrow / try_borrow_mut — they return a Result instead of panicking, which is what you want inside a Drop impl or any code path that already might be re-entering itself.

A real pattern: shared mutable state through Rc<RefCell<_>>

The canonical use is sharing one piece of mutable state between several owners on a single thread — a cache, a config, an observer list:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Default)]
struct Cache {
    hits: u32,
    keys: Vec<String>,
}

let cache = Rc::new(RefCell::new(Cache::default()));

let writer = Rc::clone(&cache);
let reader = Rc::clone(&cache);

writer.borrow_mut().keys.push("a".into());
writer.borrow_mut().hits += 1;

let snapshot = reader.borrow();
assert_eq!(snapshot.hits, 1);
assert_eq!(snapshot.keys, vec!["a".to_string()]);

Keep borrow_mut() scopes short — release the RefMut (let it drop) before calling any code that might try to borrow the same cell again. The most common “works in tests, panics in prod” bug with RefCell is a long-lived RefMut colliding with a callback that re-enters.

When to pick RefCell vs Cell

Cell<T> if swapping or copying whole values is enough — counters, flags, small Copy state. Zero overhead, no panic risk.

RefCell<T> when the inside is a real data structure you want to call methods on in place: Vec, String, HashMap, your own structs. You pay one extra word for the borrow flag and runtime checks, but you get the full &T / &mut T API back.

For multi-threaded shared state, neither of these works — both are !Sync. Reach for Mutex<T> (covered in tomorrow’s morning bite) or RwLock<T> instead.

151. Cell<T> — Interior Mutability Without the Borrow Checker Drama

You hand the same struct to two closures and both want to bump a counter. &mut fights you, RefCell introduces runtime borrow checks you don’t need — Cell<T> quietly mutates through a shared & reference, with zero overhead, as long as you only ever swap whole values in and out.

The pain: shared & and a counter

Two closures, one counter, one immutable reference:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Counter {
    hits: u32,
}

let counter = Counter { hits: 0 };
let bump = || {
    // ERROR: cannot assign to `counter.hits`, which is behind a `&` reference
    // counter.hits += 1;
};
bump();

Fn closures only capture &, so a plain field is read-only. You could redesign for FnMut and a single owner, but the moment you have two observers or a callback registry, you need interior mutability.

The fix: Cell<T> mutates through &

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::cell::Cell;

struct Counter {
    hits: Cell<u32>,
}

let counter = Counter { hits: Cell::new(0) };

let bump = || counter.hits.set(counter.hits.get() + 1);
let read = || counter.hits.get();

bump();
bump();
bump();
assert_eq!(read(), 3);

Cell::new wraps the value; get returns a copy (so T: Copy for that method); set overwrites. Both take &self — no &mut anywhere — which is why this works inside Fn closures, inside Rc, inside any structure that hands out shared references.

The invariant: you can never borrow the inside

This is the single rule that makes Cell<T> sound without runtime checks: you cannot get a reference to the value inside, only copies and swaps. There is no cell.as_ref(), no cell.deref(). The compiler enforces this — there’s nothing to alias, so the optimizer is free to assume the inner value can’t change underneath an outstanding &T.

That’s also why Cell<T> is !Sync — fine for one thread, but two threads racing on set would tear the value. For threads, reach for Atomic* (scalars) or Mutex<T> (the rest).

replace and take work for non-Copy types too

get requires T: Copy, but Cell works with String, Vec, anything — you just have to move the value out instead of copying it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::cell::Cell;

let slot: Cell<String> = Cell::new(String::from("hello"));

// `replace` swaps in a new value, returns the old one.
let old = slot.replace(String::from("world"));
assert_eq!(old, "hello");

// `take` is `replace(Default::default())`.
let now: String = slot.take();
assert_eq!(now, "world");
assert_eq!(slot.into_inner(), ""); // default String

This is the trick people miss: Cell<Vec<T>> is perfectly usable for a shared, append-only-ish buffer — you just swap the whole Vec in and out.

When to pick Cell vs RefCell

Cell<T> if you only need to swap whole values: counters, flags, configuration knobs, small Copy state, or any field where replace/take is enough. Zero runtime overhead, no panic risk.

RefCell<T> (tomorrow afternoon’s bite) if you need to borrow the inside — call &mut self methods on a Vec in place, hand a &str slice to a caller, anything where copying or swapping the whole value would be wrong. You pay for runtime borrow tracking and risk a panic, but you get back the ability to use the value normally.

Default to Cell when it fits — it almost always does for simple shared-state tweaks, and the “no inner references” rule turns out to be exactly what you wanted anyway.

147. Cell::as_array_of_cells — Mutate One Slot of a Cell-Wrapped Array

You have a Cell<[i32; 4]> and you want to bump element [2]. cell.get(), mutate the copy, cell.set(...) the whole thing back — for one slot? Cell::as_array_of_cells hands you &[Cell<i32>; 4] so each slot is its own little Cell.

The setup

Cell<T> gives you interior mutability for Copy types: &Cell<T> lets you swap the inner value through a shared reference. That’s lovely for a scalar, but the moment T is an array it becomes awkward — Cell only exposes get() and set() for the entire T:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::Cell;

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);

    // Want to bump index 2. The natural reach...
    // scores[2] += 1;            // can't index through Cell
    // scores.get()[2] += 1;       // mutating a temporary copy — does nothing

    // The "real" old way: copy out, mutate, copy back.
    let mut arr = scores.get();
    arr[2] += 1;
    scores.set(arr);

    assert_eq!(scores.get(), [10, 20, 31, 40]);
}

Three lines and a full-array copy in each direction — just to add 1. It also doesn’t compose: if you wanted to hand a single slot to another function, you’d have to pass the whole Cell<[i32; 4]> plus an index, and trust the callee to put the array back.

Enter as_array_of_cells

Stabilized in Rust 1.91, Cell::as_array_of_cells reinterprets &Cell<[T; N]> as &[Cell<T>; N]:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::cell::Cell;

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);
    let slots: &[Cell<i32>; 4] = scores.as_array_of_cells();

    // Each element is now its own Cell — mutate one without touching the others.
    slots[2].set(slots[2].get() + 1);

    assert_eq!(scores.get(), [10, 20, 31, 40]);
}

No copy, no set of the whole array. The cast is free at runtime — Cell<T> is #[repr(transparent)] over T, so a Cell<[T; N]> and a [Cell<T>; N] have identical layout. The standard library just gives you the safe view of that fact.

Pair it with Cell::update

Cell::update is the obvious dance partner — read-modify-write in one call, on a single slot:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::cell::Cell;

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);

    for slot in scores.as_array_of_cells() {
        slot.update(|n| n * 2);
    }

    assert_eq!(scores.get(), [20, 40, 60, 80]);
}

That’s the loop you actually wanted. No RefCell, no runtime borrow check, no panic risk.

Hand out a single slot

Because each element is a real &Cell<T>, you can pass one slot to another function and let it mutate just that slot — the rest of the array is untouched and the caller keeps full access:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::cell::Cell;

fn bump(slot: &Cell<i32>) {
    slot.update(|n| n + 100);
}

fn main() {
    let scores = Cell::new([10, 20, 30, 40]);
    let slots = scores.as_array_of_cells();

    bump(&slots[1]);
    bump(&slots[3]);

    assert_eq!(scores.get(), [10, 120, 30, 140]);
}

Try expressing that with cell.get() / cell.set() — you can’t, not without rebuilding the array on every call.

Slices too

There’s a sibling for unsized arrays: Cell::as_slice_of_cells turns &Cell<[T]> into &[Cell<T>]. Useful when the length isn’t known at compile time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::cell::Cell;

fn zero_out(buf: &Cell<[u8]>) {
    for slot in buf.as_slice_of_cells() {
        slot.set(0);
    }
}

fn main() {
    let buf: Cell<[u8; 5]> = Cell::new([1, 2, 3, 4, 5]);
    // &Cell<[u8; 5]> coerces to &Cell<[u8]> at the call site.
    zero_out(&buf);
    assert_eq!(buf.get(), [0, 0, 0, 0, 0]);
}

And as of Rust 1.95, both views also implement AsRef, so generic code can take impl AsRef<[Cell<T>]> and accept either form.

The signatures

1
2
3
4
5
6
7
impl<T, const N: usize> Cell<[T; N]> {
    pub const fn as_array_of_cells(&self) -> &[Cell<T>; N];
}

impl<T> Cell<[T]> {
    pub fn as_slice_of_cells(&self) -> &[Cell<T>];
}

Both are zero-cost reinterpretations — pure type-system moves, no copying. Reach for them any time you find yourself doing the get / mutate / set two-step on a Cell that wraps a collection.

54. Cell::update — Modify Interior Values Without the Gymnastics

Tired of writing cell.set(cell.get() + 1) every time you want to tweak a Cell value? Rust 1.88 added Cell::update — one call to read, transform, and write back.

The old way

Cell<T> gives you interior mutability for Copy types, but updating a value always felt clunky:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::cell::Cell;

fn main() {
    let counter = Cell::new(0u32);

    // Read, modify, write back — three steps for one logical operation
    counter.set(counter.get() + 1);
    counter.set(counter.get() + 1);
    counter.set(counter.get() + 1);

    assert_eq!(counter.get(), 3);
    println!("Counter: {}", counter.get());
}

You’re calling .get() and .set() in the same expression, which is repetitive and visually noisy — especially when the transformation is more complex than + 1.

Enter Cell::update

Stabilized in Rust 1.88, update takes a closure that receives the current value and returns the new one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::cell::Cell;

fn main() {
    let counter = Cell::new(0u32);

    counter.update(|n| n + 1);
    counter.update(|n| n + 1);
    counter.update(|n| n + 1);

    assert_eq!(counter.get(), 3);
    println!("Counter: {}", counter.get());
}

One call. No repetition of the cell name. The intent — “increment this value” — is immediately clear.

Beyond simple increments

update shines when the transformation is more involved:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::cell::Cell;

fn main() {
    let flags = Cell::new(0b0000_1010u8);

    // Toggle bit 0
    flags.update(|f| f ^ 0b0000_0001);
    assert_eq!(flags.get(), 0b0000_1011);

    // Clear the top nibble
    flags.update(|f| f & 0b0000_1111);
    assert_eq!(flags.get(), 0b0000_1011);

    // Saturating shift left
    flags.update(|f| f.saturating_mul(2));
    assert_eq!(flags.get(), 22);

    println!("Flags: {:#010b}", flags.get());
}

Compare that to flags.set(flags.get() ^ 0b0000_0001) — the update version reads like a pipeline of transformations.

A practical example: tracking state in callbacks

Cell::update is especially handy inside closures where you need shared mutable state without reaching for RefCell:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::Cell;

fn main() {
    let total = Cell::new(0i64);

    let prices = [199, 450, 85, 320, 1200];
    let discounted: Vec<i64> = prices.iter().map(|&price| {
        let final_price = if price > 500 { price * 9 / 10 } else { price };
        total.update(|t| t + final_price);
        final_price
    }).collect();

    assert_eq!(discounted, vec![199, 450, 85, 320, 1080]);
    assert_eq!(total.get(), 2134);
    println!("Prices: {:?}, Total: {}", discounted, total.get());
}

No RefCell, no runtime borrow checks, no panics — just a clean in-place update.

The signature

1
2
3
impl<T: Copy> Cell<T> {
    pub fn update(&self, f: impl FnOnce(T) -> T);
}

Note the T: Copy bound — this works because Cell copies the value out, passes it to your closure, and copies the result back in. If you need this for non-Copy types, you’ll still want RefCell.

Simple, ergonomic, and long overdue. Available since Rust 1.88.0.