Rust 100 🦀 Interior Mutability Mechanics and UnsafeCell (UnsafeCell)
Rust’s foundational aliasing rule is simple to state: a value can have either many shared references or exactly one mutable reference, never both. The compiler enforces this statically, and the optimizer relies on it to generate fast code. But some programs need mutation through a shared reference—a cache that updates on read, a reference-counted pointer that increments a counter, a mock object that records calls through an &self method. Interior mutability is the sanctioned escape from the shared-reference immutability rule, and UnsafeCell<T> is the only primitive in the language that makes it possible.
This chapter begins with the problem UnsafeCell solves: the compiler’s assumption that &T points to immutable data, and what happens when that assumption is wrong. It then examines the contract that UnsafeCell imposes on the programmer, the distinction between shared-access mutation and exclusive-access uniqueness, and the memory-layout consequences of opting out of niche optimizations. The later sections cover how the standard library’s safe abstractions—Cell, RefCell, Mutex, atomics—are built on top of UnsafeCell, and why those abstractions exist rather than encouraging direct use of the primitive.
Key point: UnsafeCell<T> does not make mutation through &T safe. It makes it possible. The programmer must still ensure that no &mut T coexists with any other reference to the contents, and that concurrent access is synchronized.
Why UnsafeCell exists
The optimizer’s immutability assumption. When the compiler sees a &T, it is entitled to assume that the data behind the reference does not change for the lifetime of that reference. This assumption enables optimizations: a value loaded through &T can be cached in a register, a loop condition can be hoisted, a read can be reordered or eliminated. If the data were mutated through an alias, those optimizations would produce incorrect results. In C, the const keyword makes a similar promise, but it is advisory and frequently violated. Rust’s &T is a stronger promise, and the compiler enforces it through the type system .
The need for controlled violation. Some data structures cannot be expressed without mutation through a shared reference. A reference-counted pointer must increment its count whenever it is cloned, and Clone takes &self. A graph’s node may cache a computed shortest path, and the cache must be updated during a read-only traversal. A mock object in a test must record calls, but the trait method takes &self. These are not hacks or workarounds; they are legitimate patterns where mutation is an implementation detail of a logically immutable operation .
The single legal opt-out. UnsafeCell<T> is the only type in Rust that disables the shared-reference immutability guarantee. Every other interior-mutability type—Cell, RefCell, Mutex, RwLock, AtomicUsize—contains an UnsafeCell internally. The standard library does not expose a second mechanism because a second mechanism would fragment the aliasing model: the compiler needs exactly one marker it can recognize as “this memory may change through a shared reference” . UnsafeCell is that marker.
The exclusive-access exception. UnsafeCell does not weaken the uniqueness guarantee of &mut T. A &mut UnsafeCell<T> is still an exclusive reference: no other reference to the cell, shared or exclusive, may coexist with it for its duration. The cell’s magic applies only to shared accesses through &UnsafeCell<T>. This asymmetry is deliberate: if UnsafeCell relaxed the &mut rule, there would be no way to reason about aliasing at all .
The memory-layout cost. UnsafeCell<T> has the same size and alignment as T, but it disables niche optimizations. The type Option<NonNull<u8>> is typically 8 bytes on 64-bit platforms because NonNull uses the null value as the None discriminant. The type Option<UnsafeCell<NonNull<u8>>> is 16 bytes because UnsafeCell prevents the compiler from using that niche. This is the price of interior mutability: the compiler cannot assume that the inner value is immutable, so it cannot reason about invalid bit patterns in the same way .
a. The contract: what UnsafeCell permits and what it forbids
The UnsafeCell API is minimal: new(value) constructs one, get() returns a *mut T, raw_get() returns a *mut T from a shared reference without creating an intermediate reference, and into_inner() unwraps the value. The simplicity is deliberate. The primitive does not provide any safety mechanism; it provides a raw pointer and a contract that the programmer must uphold .
The contract has three parts. First, mutating through the pointer obtained from get() is permitted even while other &UnsafeCell<T> references exist, provided no &mut T to the contents is active. Second, creating a &mut T from the raw pointer requires that no other reference to the contents exists for the duration of that &mut T. Third, if the cell is shared across threads, all access must be synchronized; UnsafeCell itself does not provide any thread-safety .
use std::cell::UnsafeCell;
// A minimal interior-mutability wrapper.
// The invariant: no &mut T may coexist with any other reference
// to the contents. The wrapper enforces this by only exposing
// mutation through a single path, but the compiler cannot check it.
struct Counter {
value: UnsafeCell<u64>,
}
impl Counter {
fn new(initial: u64) -> Self {
Counter { value: UnsafeCell::new(initial) }
}
// Read through a shared reference. No mutation occurs here,
// but the compiler cannot assume the value is immutable
// because it lives inside an UnsafeCell.
fn get(&self) -> u64 {
// Safety: no mutation occurs during this read, and the
// caller holds only a shared reference, so no &mut T
// to the contents can exist.
unsafe { *self.value.get() }
}
// Mutate through a shared reference. This is the operation
// that UnsafeCell makes possible but does not make safe.
fn increment(&self) {
// Safety: the wrapper guarantees (by convention) that
// no other reference to the contents exists at this point.
unsafe { *self.value.get() += 1; }
}
}
The increment method is unsound if two threads call it concurrently, or if a caller somehow holds a &mut u64 obtained through another path. The wrapper’s safety depends on an invariant that the programmer must maintain: the UnsafeCell is never aliased with a &mut to its contents. The compiler cannot verify this invariant, which is why UnsafeCell::get is an unsafe function .
The distinction between get() and raw_get() matters for one subtle reason: get() takes &self and returns *mut T, which means it creates a shared reference to the cell. raw_get() takes *const Self and avoids creating the intermediate reference. This is relevant when the cell is being deallocated concurrently, but for most code get() is the correct choice .
b. The standard library’s safe abstractions
UnsafeCell is a building block, not a user-facing tool. Direct use requires unsafe and a proof of non-aliasing that most programs cannot easily provide. The standard library’s interior-mutability types wrap UnsafeCell and provide APIs whose safety is established by construction, by runtime checks, or by hardware support.
Cell<T> is the simplest. It wraps an UnsafeCell<T> and exposes get, set, replace, and take. The key restriction is that Cell never hands out a reference to its contents. get requires T: Copy and returns a copy; set replaces the value; replace swaps it out. Because no reference is ever created, the aliasing invariant is trivially maintained: there is no way to hold a &T or &mut T to the contents through the Cell API .
use std::cell::Cell;
// Cell is for Copy types and whole-value replacement.
// It never gives out a reference to the inner value.
let counter = Cell::new(0u64);
// Read by copying the value out.
let current = counter.get();
// Write by replacing the value. No reference is created.
counter.set(current + 1);
// The value can be modified through a shared reference.
assert_eq!(counter.get(), 1);
RefCell<T> is for non-Copy types and for cases where a reference to the contents is needed. It tracks borrows at runtime with a counter: borrow() increments the shared-borrow count and returns a Ref<T> guard; borrow_mut() checks that the count is zero, sets the exclusive flag, and returns a RefMut<T> guard. When a guard is dropped, the count is decremented or the exclusive flag is cleared. If borrow_mut is called while any borrow is active, the thread panics with BorrowMutError .
use std::cell::RefCell;
// RefCell is for non-Copy types and for borrowing the contents.
let log = RefCell::new(Vec::new());
// Multiple shared borrows are allowed.
{
let reader1 = log.borrow();
let reader2 = log.borrow();
assert_eq!(reader1.len(), 0);
}
// One mutable borrow, with no shared borrows active.
{
let mut writer = log.borrow_mut();
writer.push("event");
}
// The panic case: two mutable borrows at once.
// let mut a = log.borrow_mut();
// let mut b = log.borrow_mut(); // panic: already borrowed
The runtime check is the cost of RefCell. A Cell operation is a move or copy with no overhead beyond the UnsafeCell itself. A RefCell operation involves an atomic or non-atomic counter update and a branch, plus the panic path if the check fails. For single-threaded code, the counter is a plain Cell<isize>, so the overhead is a few instructions; the real cost is the loss of compile-time guarantees. A borrow conflict that would be a compile error with & and &mut becomes a runtime panic with RefCell .
Mutex<T> and RwLock<T> extend the same pattern to multiple threads. Mutex uses an OS lock to ensure that only one thread holds the &mut T at a time; RwLock allows multiple readers or one writer. Atomics (AtomicUsize, AtomicBool, etc.) use hardware instructions to provide lock-free interior mutability for specific scalar types. These are all built on UnsafeCell, but their safety comes from synchronization primitives rather than from single-threaded non-aliasing invariants .
c. The aliasing rules in practice
The UnsafeCell documentation enumerates the legal aliasing scenarios for single-threaded code. A &T can coexist with other &T references, but not with a &mut T. A &mut T must be unique: no other &mut T and no &T may coexist with it. These are the same rules that apply to ordinary Rust references, with one exception: a &UnsafeCell<T> can coexist with mutation of the T inside, provided the mutation does not create a &mut T that violates the uniqueness rule .
The exception is narrow. Mutating through a raw pointer derived from &UnsafeCell<T> is permitted while other &UnsafeCell<T> references exist. But if the programmer converts that raw pointer to a &mut T and holds it, no other reference to the contents may exist for the duration of the &mut T. The following example is legal:
use std::cell::UnsafeCell;
let cell = UnsafeCell::new(42);
// Two shared references to the cell.
let r1: &UnsafeCell<i32> = &cell;
let r2: &UnsafeCell<i32> = &cell;
// Mutation through r1's raw pointer is legal because no &mut T exists.
unsafe {
*r1.get() = 100;
}
// Reading through r2 sees the mutation.
unsafe {
assert_eq!(*r2.get(), 100);
}
The following example is undefined behavior:
// UB: two &mut i32 aliases to the same contents.
unsafe {
let m1: &mut i32 = &mut *r1.get();
let m2: &mut i32 = &mut *r2.get();
*m1 = 1;
*m2 = 2; // UB: m1 and m2 alias
}
The second example violates the uniqueness rule for &mut T. UnsafeCell does not relax that rule. The cell’s special behavior applies only to shared references: a &UnsafeCell<T> permits mutation of the T inside, but a &mut UnsafeCell<T> is still an exclusive reference to the cell, and a &mut T derived from the cell is still an exclusive reference to the contents .
The get_mut method on UnsafeCell is the safe accessor for exclusive access. It takes &mut self and returns &mut T. This is safe because the &mut self already guarantees exclusivity: no other reference to the cell can exist while the &mut self is active, so the &mut T is also exclusive .
Complete Example Session
// ============================================
// PART 1: THE SETUP
// ============================================
// We build a small observable counter to demonstrate
// UnsafeCell directly, then show why the safe abstractions
// exist by comparing the invariants each enforces.
use std::cell::{Cell, RefCell, UnsafeCell};
// ============================================
// PART 2: THE RAW UnsafeCell WRAPPER
// ============================================
// This wrapper is sound only if the caller never holds
// a &mut to the contents while other references exist.
// The wrapper enforces this by never exposing a reference.
struct RawCounter {
value: UnsafeCell<u64>,
}
impl RawCounter {
fn new() -> Self {
RawCounter { value: UnsafeCell::new(0) }
}
fn get(&self) -> u64 {
// Safety: the wrapper's API never creates a &mut to
// the contents, so no exclusive alias can exist here.
unsafe { *self.value.get() }
}
fn add(&self, n: u64) {
// Safety: same invariant as get. The wrapper serializes
// all access through &self and never creates a reference.
unsafe { *self.value.get() += n; }
}
}
// ============================================
// PART 3: THE Cell VERSION
// ============================================
// Cell provides the same functionality safely by
// never handing out a reference to the inner value.
struct CellCounter {
value: Cell<u64>,
}
impl CellCounter {
fn new() -> Self {
CellCounter { value: Cell::new(0) }
}
fn get(&self) -> u64 {
self.value.get() // copy out, no reference
}
fn add(&self, n: u64) {
self.value.set(self.value.get() + n); // replace, no reference
}
}
// ============================================
// PART 4: THE RefCell VERSION
// ============================================
// RefCell provides interior mutability with references,
// tracked at runtime. Useful when the value is not Copy.
struct RefCellCounter {
value: RefCell<Vec<u64>>,
}
impl RefCellCounter {
fn new() -> Self {
RefCellCounter { value: RefCell::new(Vec::new()) }
}
fn record(&self, n: u64) {
self.value.borrow_mut().push(n); // runtime-checked &mut
}
fn total(&self) -> u64 {
self.value.borrow().iter().sum() // runtime-checked &T
}
}
// ============================================
// PART 5: THE Rc<RefCell> PATTERN
// ============================================
// Multiple owners plus interior mutability.
// This is the standard solution for shared mutable state
// in single-threaded code.
use std::rc::Rc;
fn rc_refcell_example() {
let shared = Rc::new(RefCell::new(Vec::new()));
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
clone1.borrow_mut().push(1);
clone2.borrow_mut().push(2);
assert_eq!(shared.borrow().len(), 2);
}
// ============================================
// PART 6: THE MUTEX EQUIVALENT
// ============================================
// For multi-threaded code, Mutex provides the same
// interior mutability with OS-level synchronization.
use std::sync::Mutex;
fn mutex_example() {
let shared = Mutex::new(0u64);
// Lock before accessing, unlock when the guard drops.
let mut guard = shared.lock().unwrap();
*guard += 1;
drop(guard);
assert_eq!(*shared.lock().unwrap(), 1);
}
// ============================================
// PART 7: THE ATOMIC EQUIVALENT
// ============================================
// Atomics provide lock-free interior mutability
// for scalar types, with hardware synchronization.
use std::sync::atomic::{AtomicU64, Ordering};
fn atomic_example() {
let counter = AtomicU64::new(0);
counter.fetch_add(1, Ordering::SeqCst);
counter.fetch_add(1, Ordering::SeqCst);
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
// ============================================
// PART 8: THE LAYOUT COST
// ============================================
// UnsafeCell disables niche optimizations.
// The following assertions show the size difference.
use std::ptr::NonNull;
fn layout_facts() {
// Option<NonNull<u8>> uses the null niche: 8 bytes.
assert_eq!(
std::mem::size_of::<Option<NonNull<u8>>>(),
8
);
// Option<UnsafeCell<NonNull<u8>>> cannot use the niche: 16 bytes.
assert_eq!(
std::mem::size_of::<Option<UnsafeCell<NonNull<u8>>>>(),
16
);
}
// ============================================
// PART 9: THE ALIASING RULES
// ============================================
// The raw UnsafeCell allows mutation through shared
// references, but not exclusive aliases.
fn aliasing_example() {
let cell = UnsafeCell::new(10);
let r1 = &cell;
let r2 = &cell;
// Legal: mutation through a shared reference.
unsafe { *r1.get() = 20; }
// Legal: reading through another shared reference.
unsafe { assert_eq!(*r2.get(), 20); }
// Legal: exclusive access through get_mut,
// because no other reference exists at this point.
let exclusive: &mut i32 = cell.get_mut();
*exclusive = 30;
}
// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// UnsafeCell is the primitive. Cell, RefCell, Mutex,
// and atomics are the safe interfaces. Each makes a
// different trade-off between restrictions and overhead.
fn main() {
let raw = RawCounter::new();
raw.add(5);
assert_eq!(raw.get(), 5);
let cell = CellCounter::new();
cell.add(3);
assert_eq!(cell.get(), 3);
let rc = RefCellCounter::new();
rc.record(10);
rc.record(20);
assert_eq!(rc.total(), 30);
rc_refcell_example();
mutex_example();
atomic_example();
layout_facts();
aliasing_example();
}
The ten parts build a comparison of every interior-mutability mechanism in the standard library, anchored by the raw UnsafeCell wrapper. The pattern is consistent: UnsafeCell provides the capability, and each safe abstraction imposes a different restriction to make the capability sound. Cell restricts by forbidding references, RefCell restricts by runtime checking, Mutex restricts by blocking, and atomics restrict by limiting operations to hardware-supported primitives.
Quick Reference
Interior-Mutability Types
| Type | Thread-Safe | References | Runtime Check | Primary Use |
|---|---|---|---|---|
UnsafeCell<T> | No | Raw only | None | Building block |
Cell<T> | No | Never | None | Copy values |
RefCell<T> | No | Yes (Ref/RefMut) | Panic on conflict | Non-Copy, single-thread |
Mutex<T> | Yes | Yes (MutexGuard) | Blocks | Shared mutable state |
RwLock<T> | Yes | Yes (RwLockReadGuard) | Blocks | Read-heavy shared state |
AtomicU64 | Yes | No | Hardware | Lock-free scalars |
UnsafeCell Methods
| Method | Safety | Returns |
|---|---|---|
new(value) | Safe | UnsafeCell<T> |
get(&self) | Unsafe to deref | *mut T |
raw_get(*const Self) | Unsafe to deref | *mut T |
get_mut(&mut self) | Safe | &mut T |
into_inner(self) | Safe | T |
Aliasing Rules
| Scenario | Legal? |
|---|---|
&T + &T | Yes |
&T + &mut T | No |
&mut T + &mut T | No |
&UnsafeCell<T> + mutation via get() | Yes (if no &mut T exists) |
&mut UnsafeCell<T> + any other reference | No |
Layout Consequences
| Type | Size (x86-64) |
|---|---|
NonNull<u8> | 8 |
Option<NonNull<u8>> | 8 (niche) |
UnsafeCell<NonNull<u8>> | 8 |
Option<UnsafeCell<NonNull<u8>>> | 16 (no niche) |
Best Practices
✅ Do This:
// Use Cell for Copy types and whole-value replacement.
let counter = Cell::new(0); // ✅
// Use RefCell for non-Copy types when references are needed.
let log = RefCell::new(Vec::new()); // ✅
// Use Mutex for cross-thread shared state.
let shared = Mutex::new(0); // ✅
// Use atomics for lock-free scalar counters.
let counter = AtomicU64::new(0); // ✅
// Document the invariant when using UnsafeCell directly.
// Invariant: no &mut to contents while any other reference exists. // ✅
// Use get_mut for exclusive access, avoiding raw pointers.
let value: &mut T = cell.get_mut(); // ✅
// Wrap only the field that needs interior mutability.
struct S { cache: RefCell<Option<Vec<u8>>>, name: String } // ✅
❌ Don’t Do This:
// Don't use UnsafeCell directly when a safe abstraction works.
struct S { value: UnsafeCell<u64> } // use Cell instead // ❌
// Don't create two &mut T from the same UnsafeCell.
let m1 = &mut *cell.get(); let m2 = &mut *cell.get(); // ❌
// Don't assume UnsafeCell makes multi-threaded access safe.
// It does not. Use Mutex, RwLock, or atomics. // ❌
// Don't use RefCell for cross-thread code.
let shared = Rc::new(RefCell::new(0)); // not Sync // ❌
// Don't ignore the layout cost in performance-critical types.
Option<UnsafeCell<NonNull<T>>> // 16 bytes, not 8 // ❌
// Don't use interior mutability when &mut self would work.
fn update(&self) { self.field.borrow_mut()... } // prefer &mut self // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Undefined behavior from aliased &mut | Two &mut T derived from the same UnsafeCell | Ensure exclusivity before creating &mut T |
RefCell panic in production | Two mutable borrows overlap at runtime | Scope guards tightly; use try_borrow_mut for fallible paths |
| Data race across threads | UnsafeCell used without synchronization | Use Mutex, RwLock, or atomics for shared state |
| Type size unexpectedly large | UnsafeCell disables niche optimization | Avoid wrapping pointers in UnsafeCell inside Option |
| Unsound wrapper | UnsafeCell exposed without invariant enforcement | Never expose &mut T; restrict the API to sound operations |
Cell cannot hold non-Copy | Cell::get requires Copy | Use RefCell or Cell::take/replace for non-Copy |
| Borrow panic from long-lived guard | RefMut held across unrelated code | Drop the guard explicitly before other borrows |
Real-World Examples
1. Reference Counting
struct RcBox<T> {
value: T,
count: Cell<usize>, // interior mutability for Clone
}
2. Caching in a Read-Only API
struct Graph {
cache: RefCell<Option<Vec<u32>>>, // updated through &self
}
3. Mock Object Recording Calls
struct Mock {
calls: RefCell<Vec<String>>, // &self method must record
}
4. Lazy Initialization
struct Lazy {
value: OnceCell<String>, // set once through &self
}
5. Cross-Thread Counter
let shared = Arc::new(AtomicU64::new(0));
shared.fetch_add(1, Ordering::Relaxed);
6. Cross-Thread Mutable State
let shared = Arc::new(Mutex::new(Vec::new()));
shared.lock().unwrap().push(1);
7. Raw UnsafeCell Wrapper
struct Raw<T> { value: UnsafeCell<T> }
unsafe impl<T: Send> Send for Raw<T> {}
8. Exclusive Access Recovery
let value: &mut T = cell.get_mut(); // safe, &mut self
9. Bypassing Niche Cost
// Avoid: Option<UnsafeCell<NonNull<T>>>
// Prefer: UnsafeCell<Option<NonNull<T>>>
10. Fallible RefCell Borrow
if let Ok(mut guard) = cell.try_borrow_mut() {
guard.push(1);
}
Visual
The Compiler’s Assumption and the Escape
┌─────────────────────────────────────────────────────────┐
│ ORDINARY &T │
│ │
│ let x = 42; │
│ let r = &x; │
│ │
│ Compiler: "r points to immutable data. I can │
│ cache the load, hoist it out of loops, reorder it." │
│ │
│ Mutation through another path: │
│ ═══════════════════════════════════════════════════ │
│ ❌ UNDEFINED BEHAVIOR │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ UnsafeCell<T> │
│ │
│ let cell = UnsafeCell::new(42); │
│ let r = &cell; │
│ │
│ Compiler: "The contents of this cell may change │
│ through a shared reference. I cannot assume │
│ immutability. No caching, no hoisting." │
│ │
│ Mutation through r.get(): │
│ ═══════════════════════════════════════════════════ │
│ ✅ PERMITTED (if no &mut T exists) │
└─────────────────────────────────────────────────────────┘
The Interior-Mutability Hierarchy
┌─────────────────────────────────────────────────────────┐
│ UnsafeCell<T> │
│ ┌───────────────────────────────────────────────────┐ │
│ │ The primitive. No safety mechanism. │ │
│ │ Provides *mut T. Programmer upholds the contract.│ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Cell<T> │ │ RefCell<T>│ │ Atomic* │ │
│ │ │ │ │ │ │ │
│ │ No refs │ │ Runtime │ │ Hardware │ │
│ │ Copy only │ │ borrow │ │ sync │ │
│ │ │ │ check │ │ │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Single │ │ Single │ │ Multi- │ │
│ │ thread │ │ thread │ │ thread │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ For multi-thread: Mutex<T>, RwLock<T> │
│ (also built on UnsafeCell + OS synchronization) │
└─────────────────────────────────────────────────────────┘
The Niche Cost
┌─────────────────────────────────────────────────────────┐
│ Option<NonNull<u8>> │
│ │
│ NonNull<u8> is a non-null pointer: 8 bytes. │
│ The null value is not a valid NonNull, so Option │
│ uses it as the None discriminant. │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ Some(ptr) │ ptr value (non-null) │ 8 bytes │
│ │ None │ 0x0000000000000000 │ │
│ └───────────────────────────────────────────┘ │
│ │
│ Total: 8 bytes. The niche is free. │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ Option<UnsafeCell<NonNull<u8>>> │
│ │
│ UnsafeCell disables the niche: the compiler cannot │
│ assume the inner value is immutable, so it cannot │
│ assume any bit pattern is invalid. │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ discriminant (0 or 1) │ padding │ 8 bytes │
│ │ ptr value │ │ 8 bytes │
│ └───────────────────────────────────────────┘ │
│ │
│ Total: 16 bytes. The niche is lost. │
└─────────────────────────────────────────────────────────┘
The Aliasing Contract
┌─────────────────────────────────────────────────────────┐
│ UnsafeCell<T> ALLOWS: │
│ │
│ &UnsafeCell<T> + &UnsafeCell<T> + mutation │
│ (shared refs) (shared refs) via get() │
│ │
│ ✅ Legal if no &mut T to the contents exists. │
│ │
│ UnsafeCell<T> FORBIDS: │
│ │
│ &mut T + any other reference to the contents │
│ │
│ ❌ UB even with UnsafeCell. │
│ │
│ UnsafeCell<T> DOES NOT AFFECT: │
│ │
│ &mut UnsafeCell<T> + any other reference │
│ │
│ ❌ UB. The cell itself is exclusive when accessed │
│ through &mut. │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Purpose | Disable shared-reference immutability guarantee |
| Safety | No safety mechanism; programmer upholds contract |
get() | Returns *mut T from &self |
get_mut() | Returns &mut T from &mut self (safe) |
| Aliasing | Shared refs + mutation allowed; &mut T must be unique |
| Thread safety | None; UnsafeCell is !Sync |
| Niche cost | Disables niche optimization in enclosing types |
Cell | Built on UnsafeCell; no references, Copy only |
RefCell | Built on UnsafeCell; runtime borrow checking |
Mutex/atomics | Built on UnsafeCell plus synchronization |
Key takeaways:
UnsafeCellis the only legal opt-out of the shared-reference immutability rule. Every safe interior-mutability type contains one.- The primitive does not make mutation safe. It makes mutation possible. The programmer must ensure no
&mut Taliases exist and that cross-thread access is synchronized. - The uniqueness of
&mut Tis not relaxed. A&mut UnsafeCell<T>is exclusive, and a&mut Tderived from the cell must be unique. Cellavoids references entirely. It is the safest and cheapest interior-mutability type forCopyvalues and whole-value replacement.RefCelltrades compile-time checking for runtime checking. Borrow conflicts become panics instead of compile errors, and the check has a small runtime cost.Mutexand atomics extend interior mutability to threads. They use OS locks or hardware instructions to enforce the invariants thatUnsafeCellleaves to the programmer.- Niche optimization is disabled.
UnsafeCelltells the compiler that the inner value may change, so invalid bit patterns cannot be assumed absent. - Direct
UnsafeCelluse is rare. It belongs in the implementation of a safe abstraction, not in application code. - The invariant must be documented. A wrapper that uses
UnsafeCellmust state, in comments or documentation, what guarantees it provides and what callers must not do. - Interior mutability is a last resort. Inherited mutability (
&mut self) is preferred when it expresses the operation. Interior mutability should make an invariant expressible, not make every part of a program mutable.
Remember: UnsafeCell<T> is the single primitive that tells the Rust compiler “this memory may change through a shared reference.” It does not provide safety; it provides permission. The standard library’s interior-mutability types—Cell, RefCell, Mutex, RwLock, atomics—are safe because they restrict the programmer’s access in ways that make the aliasing invariant enforceable: no references, runtime checks, blocking, or hardware synchronization. When you use UnsafeCell directly, you become the enforcer. The compiler steps back, and the burden of proving that no &mut T aliases exist, and that no data race occurs, falls on you. That burden is real, and it is the reason the primitive exists in the standard library’s internals rather than in everyday application code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!