Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7.3 · Cell<T> and RefCell<T>: Interior Mutability

Domain 7 — Smart Pointers and Heap Allocation Duration: ~15 minutes Library components: std::cell::Cell, std::cell::RefCell, std::cell::Ref, std::cell::RefMut, std::cell::UnsafeCell

Introduction

The aliasing rule of Rust is: shared (&T) XOR mutable (&mut T). The compiler usually enforces this rule at compile time. Interior mutability is the approved exception. It is a set of types that permit mutation through a shared reference. Each type replaces the compile-time check with a different runtime discipline.

Two reasons to use interior mutability occur frequently:

  • Bookkeeping behind an API that is logically read-only: hit counters, caches, memoization. lookup(&self) should look like a query, although it updates statistics.
  • Shared ownership that needs mutation: Rc<T> (tutorial 7.2) has no DerefMut. Rc<RefCell<T>> is the usual single-threaded combination for shared mutable data. Example 07_07 used RefCell fields in Rc nodes to connect the links of a tree.

This tutorial shows:

  • Cell<T>: values move in and out, with zero overhead.
  • RefCell<T>: borrows that the Ref/RefMut guards check at runtime. A conflict gives BorrowError or BorrowMutError.
  • The conversions between a cell of an array and an array of cells, with the AsRef impls stabilized in Rust 1.95.
  • UnsafeCell<T>: the primitive below all of these types.

Cell<T>: Values In, Values Out

The discipline of Cell<T> is very simple: no reference to the contents ever exists. You copy or move whole values in and out with set, get, replace, take, and swap. Thus there is nothing to alias and nothing to check at runtime. A Cell<T> has exactly the same cost as a bare T.

use std::cell::Cell;

struct PriceCache {
    prices: Vec<(&'static str, u32)>,  // (SKU, price in cents), immutable after construction
    hits: Cell<u32>,                   // mutated through &self
    misses: Cell<u32>,
}

impl PriceCache {
    fn lookup(&self, sku: &str) -> Option<u32> {   // &self, not &mut self
        // … find `sku` in self.prices …
        // On a hit: get copies the count out, and set moves the new count in.
        self.hits.set(self.hits.get() + 1);
        // … on a miss, self.misses counts in the same way …
    }
}

Why get requires Copy

get gives you the value while the cell keeps it too. Only types that you can duplicate (Copy types) permit that. For a type that is not Copy, Cell<String>::get fails with error[E0599]: … trait bounds were not satisfied: String: Copy. You can still use contents that are not Copy in full. You move the values and do not copy them:

let label: Cell<String> = Cell::new(String::from("draft"));

let old = label.replace(String::from("review"));  // moves "review" in, returns "draft"
assert_eq!(old, "draft");

let current = label.take();                        // moves "review" out, leaves String::default()
assert_eq!(current, "review");
assert_eq!(label.take(), "");                      // the default (empty) String was in the cell

swap exchanges the contents of two cells in place. into_inner consumes the cell and returns the value. Its by-value self proves that no other code can observe the cell after the call. Like each type in this tutorial, Cell is !Sync: it is for one thread only.

07_11_cell_basics.rs prints:

lookups done: hits = 3, misses = 1
non-Copy Cell: replace -> "draft", take -> "review"
after swap: front = "secondary", backup = "primary"

All assertions passed.

RefCell<T>: The Borrow Checker at Runtime

A Cell is not convenient when you want to call methods on the contents. Examples are a push to a Vec and the mutation of a field. RefCell<T> keeps the data in place and lends real references. It enforces the aliasing rule with a runtime counter. The counter permits any number of Ref guards (from borrow()), XOR one RefMut guard (from borrow_mut()).

Figure: RefCell Borrow States

The guards are the most important part. Each Ref or RefMut holds its borrow until it drops. Thus the lifetimes of the guards, not the statement boundaries, control which borrows conflict:

use std::cell::RefCell;

struct EventSink { events: RefCell<Vec<String>> }

impl EventSink {
    fn record(&self, event: &str) {   // &self, but the method mutates
        // borrow_mut returns a RefMut<Vec<String>> guard. The guard lives only
        // for this expression, so it cannot overlap another borrow.
        self.events.borrow_mut().push(event.to_owned());
    }
}

Ref::map projects a guard onto a component (Ref<Vec<String>> → Ref<str>). It lends part of the data, and the borrow of the whole value stays active. RefMut::map does the same for a RefMut.

try_borrow and try_borrow_mut: Conflicts as Values

A borrow() or borrow_mut() that conflicts panics (for example, with the message RefCell already borrowed). A panic is the right result for a real bug. But sometimes a conflict is a legitimate runtime condition (reentrant callbacks, observers, recursive walks). The try_ variants report the conflict as a Result:

// `sink` is an EventSink that holds three events.
let reader_a = sink.events.borrow();          // Ref<Vec<String>>
let reader_b = sink.events.borrow();          // many readers at the same time: permitted

let denied = sink.events.try_borrow_mut();    // a writer while the readers are alive
assert!(denied.is_err());                     // Err(BorrowMutError), no panic

drop(reader_a);
drop(reader_b);                               // no guard is alive now
assert!(sink.events.try_borrow_mut().is_ok()); // this temporary RefMut drops immediately

let writer = sink.events.borrow_mut();        // RefMut<Vec<String>>
assert!(sink.events.try_borrow().is_err());   // Err(BorrowError): the writer is alive

General rule: use borrow/borrow_mut inside short methods where no guard can overlap. Use try_* at boundaries where reentrancy is possible. The equivalent type for more than one thread is RwLock (tutorial 6.2), which blocks and does not panic.

07_12_refcell_runtime_borrow.rs prints:

events recorded: 3
writer while 2 readers live: Err(RefCell already borrowed)
reader while writer live:    Err(RefCell already mutably borrowed)
Ref::map projection: first event = "connected"

All assertions passed.

Arrays and Slices of Cells

Cell<T> has the same memory layout as T. Thus a set of conversions between one cell of many values and many cells is possible at no cost:

  • Cell::from_mut(&mut T) -> &Cell<T>: you hold exclusive access, and you exchange it for a cell view that you can share.
  • Cell<[T]>::as_slice_of_cells(&self) -> &[Cell<T>]: converts a cell of a slice into a slice of cells.
  • Stabilized in Rust 1.95: AsRef impls for arrays. .as_ref() converts Cell<[T; N]> directly to &[Cell<T>; N], and Cell<[T]> to &[Cell<T>].

This is important because &[Cell<T>] is a shared view that still permits writes to the elements. Different parts of the code can hold views that overlap at the same time. &mut [T] cannot do that:

// scale and clip are functions that take a shared view, for example:
//     fn scale(samples: &[Cell<i32>], factor: i32)
let mut raw = [8, 40, -3, 25, 7];
// &mut [i32] → &Cell<[i32]> → &[Cell<i32>]
let samples: &[Cell<i32>] = Cell::from_mut(&mut raw[..]).as_slice_of_cells();

scale(samples, 2);   // multiplies each element by 2
clip(samples, 50);   // clamps each element to the range -50..=50
assert_eq!(raw, [16, 50, -6, 50, 14]);   // `raw` is usable again: the cell view ended

let buffer: Cell<[i32; 6]> = Cell::new([1, -2, 3, -4, 5, -6]);
let cells: &[Cell<i32>; 6] = buffer.as_ref();   // Rust 1.95 AsRef
cells[0].set(10);                               // writes element 0 through the shared view

The best example is windows that overlap. Each iteration reads the adjacent element and writes the current element. iter_mut can never permit that:

// cells: &[Cell<i32>], a view of the array [1, 2, 4, 8, 16]
for window in cells.windows(2) {   // window: &[Cell<i32>] of length 2
    // Add the next element to the current element.
    window[0].set(window[0].get() + window[1].get());
}
// The array is now [3, 6, 12, 24, 16].

07_13_cell_arrays_slices.rs prints:

scaled + clipped: [16, 50, -6, 50, 14]
array-of-cells writes: [10, 20, 3, -4, 5, -6]
pairwise-summed in place: [3, 6, 12, 24, 16]

All assertions passed.

Choosing a Cell

std::cell also contains OnceCell<T> (write-once) and LazyCell<T> (initialized on first access). These are single-assignment disciplines. Tutorial 6.6 describes them in full, together with their thread-safe equivalents OnceLock and LazyLock. When you know all these types, the selection follows a fixed procedure:

Figure: Interior Mutability Decision Tree

Prefer Cell when it is sufficient to move whole values. It cannot panic, and it has no cost. Use RefCell when you need real references into the data. Accept that incorrect use then shows as a runtime error, not as a compile error.

UnsafeCell<T>: The Primitive Underneath

Each interior mutability type (Cell, RefCell, Mutex, RwLock, the atomics) wraps the same primitive: UnsafeCell<T>. It is the only approved way to mutate through &T. The compiler assumes that no code writes behind a & reference. It exempts &UnsafeCell<T> from this assumption, and it never marks &UnsafeCell<T> as noalias for the optimizer. A cast of &T to &mut T in any other way is undefined behavior, also inside unsafe. An unsafe block does not make that cast valid.

Its API is very small. The primary method is get(&self) -> *mut T. It is safe to call, and only the dereference of the pointer is unsafe. Each wrapper adds a discipline that makes the dereference sound:

UnsafeCell<T>   raw permission to mutate behind &T          (unsafe)
Cell<T>         + values move in/out, no references escape  → safe
RefCell<T>      + counted borrows, checked at runtime       → safe
Mutex<T>        + OS locking, opts back into Sync           → safe + threads

A miniature Cell shows the form of such a safety argument (example 07_14):

use std::cell::UnsafeCell;

struct MiniCell<T> { value: UnsafeCell<T> }

impl<T> MiniCell<T> {
    fn set(&self, value: T) {
        // self.value.get() returns *mut T. The call is safe, but the write is not.
        // SAFETY: no reference to the contents ever leaves MiniCell, and
        // UnsafeCell is !Sync, so no other thread can hold &self. Thus the
        // write cannot alias anything.
        unsafe { *self.value.get() = value; }
    }
}

The example also measures the costs. UnsafeCell<u64> and Cell<u64> are exactly 8 bytes, which is zero overhead. RefCell<u64> is 16 bytes: one more word for the borrow counter. !Sync propagates automatically from UnsafeCell. For this reason, each type in this tutorial is for one thread only. The exception is a type that explicitly becomes Sync again with locking (unsafe impl Sync, as Mutex does).

07_14_unsafecell_foundation.rs prints:

MiniCell works: counter = 2, title flipped from "draft"
sizes: u64 = 8, Cell<u64> = 8, RefCell<u64> = 16

All assertions passed.

Summary

ConceptKey point
Interior mutabilityMutation through &T. A runtime discipline enforces the aliasing rule.
Cell<T>Values move in and out (set/get/replace/take/swap). It has zero overhead and cannot panic.
get needs CopyThe cell keeps the value and also gives you one. Only types that you can duplicate permit that.
RefCell<T>Real references through counted guards: many Ref XOR one RefMut
Ref / RefMutA guard holds its borrow until it drops. Ref::map projects a guard onto a component.
try_borrow / try_borrow_mutA conflict gives Err(BorrowError) / Err(BorrowMutError), not a panic
Cell::from_mut&mut T → &Cell<T>: exchanges exclusive access for a view that you can share
as_slice_of_cells&Cell<[T]> → &[Cell<T>]. The AsRef forms for arrays are stable since Rust 1.95.
OnceCell / LazyCellWrite-once cells and lazy-init cells. Tutorial 6.6 describes them in full.
UnsafeCell<T>The primitive: the only legal channel for mutation from &T to *mut T
!SyncAll of std::cell is for one thread only. The thread-safe equivalents are in std::sync.

Code Examples

FileDescription
07_11_cell_basics.rsCell counters behind &self, get/set/replace/take/swap, and the Copy bound
07_12_refcell_runtime_borrow.rsRefCell guards, Ref::map, and BorrowError/BorrowMutError results from try_*
07_13_cell_arrays_slices.rsCell::from_mut, as_slice_of_cells, and the AsRef conversions for arrays (Rust 1.95)
07_14_unsafecell_foundation.rsA MiniCell built on UnsafeCell, safety arguments, size comparisons