This article explains the idea behind several standard library types: Cell, RefCell, OnceCell, Mutex, RwLock and the atomic integers. A common description of them is:
"
RefCellis the escape hatch you use when the borrow checker won't let you do what you want."
That description is misleading. These types do not turn the borrowing rules off. They enforce the same goal (no conflicting access to data) with a different mechanism, and for RefCell that mechanism runs while the program executes, so a mistake becomes a panic instead of a compile error. The rest of this article explains what that goal is and how each type meets it.
Shared and exclusive references
Many introductions, including The Rust Programming Language book, call &T an "immutable reference" and &mut T a "mutable reference". The Rust Reference uses the names shared reference and mutable reference. For this topic a clearer way to read them is:
&Tis a shared reference. Any number of them can point to the same value at the same time.&mut Tis an exclusive reference. While it is in use, no other reference or path (including the owner) can access that value.
Two references alias when they can reach the same value. For ordinary data the borrow checker enforces a rule often summarized as aliasing XOR mutability: a value can have several shared references, or one exclusive reference that may change it, but not both at once. The exclusivity is the property being checked. Permission to write is what exclusivity makes safe.
You can see that the check is about exclusivity by creating two &mut borrows and never writing through either:
fn main() {
let mut n = 0;
let a = &mut n;
let b = &mut n;
println!("{a} {b}");
}
The compiler rejects this with error E0499 ("cannot borrow n as mutable more than once at a time"), even though the program only reads.
Check your understanding Why does the compiler care about exclusivity rather than about writing? Show answer Hide answer
Because knowing that nothing else can touch a value lets the compiler optimize code that uses it.
While a function holds a &mut T, the compiler may assume that no other pointer reads or writes that T. It can then keep the value in a register across a function call, skip reloading it from memory, or reorder writes to it. None of that would be valid if another pointer might observe the value in between. In current rustc this information is passed to the LLVM backend as noalias attributes on many reference parameters; that is an implementation detail, but the language rules are what make it allowed.
C shows the alternative. An ordinary C pointer may alias other pointers, so the compiler must often assume that a write through one pointer could change what another one reads. C99 added the restrict qualifier so a programmer can promise that a pointer has no aliases. The compiler does not check that promise, and breaking it is undefined behavior. In Rust, &mut carries the promise and the borrow checker verifies it.
What interior mutability means
A type has interior mutability when its API lets you change the value it contains through a shared reference, usually through a method that takes &self instead of &mut self. Such a type is safe only if its API still prevents conflicting access. Here is the simplest one, Cell:
use std::cell::Cell;
fn bump(counter: &Cell<u32>) {
counter.set(counter.get() + 1);
}
fn main() {
let c = Cell::new(0);
let a = &c;
let b = &c;
bump(a);
bump(b);
assert_eq!(c.get(), 2);
}
a and b are two shared references to the same Cell, and both are used to change it. This is sound because Cell never gives out a reference to the number inside. get returns a copy and set replaces the whole value, so there is never a reference into the contents for a change to invalidate.
The standard library offers several such types. This table lists the common ones. Its last column refers to the Sync trait: a type is Sync when shared references to it can safely be used from several threads at once. The list is not complete (there are also LazyCell, OnceLock, LazyLock and others):
| type | how it prevents conflicting access | what can go wrong at run time | shareable between threads (is it Sync)? |
|---|---|---|---|
Cell<T> |
Never hands out a reference to the inside through &Cell<T>. Values are copied out (get, which needs T: Copy) or moved in and out (set, replace, take). |
Nothing; there is no borrow to conflict with. | No (Cell<T> is not Sync) |
RefCell<T> |
Counts the active borrows while the program runs. borrow and borrow_mut return guard objects that release the borrow when dropped. |
borrow_mut while any other borrow is active, or borrow while a mutable borrow is active, panics. try_borrow and try_borrow_mut return an Err instead. |
No |
OnceCell<T> |
The value can be set at most once. After that, only shared references to it are handed out. | set on a cell that already has a value returns Err. get_or_init panics if its initializer tries to initialize the same cell again. |
No |
Mutex<T>, RwLock<T> |
Locking: a thread that wants access waits until no conflicting thread holds the lock. | Waiting (blocking); deadlock if threads acquire locks in conflicting orders; locking a Mutex again from the thread that already holds it never succeeds (it may deadlock or panic). If a thread panics while holding a Mutex lock (or an RwLock write lock), the lock becomes poisoned and later lock (or read/write) calls return Err; a panic while holding only an RwLock read lock does not poison it. |
Yes, when the contents meet the documented bounds (T: Send for Mutex<T>, T: Send + Sync for RwLock<T>) |
AtomicUsize, AtomicBool, and the other atomics |
Atomic operations provided by the processor on a single integer, boolean or pointer. | No blocking. You must choose a memory ordering for each operation, and some orderings are invalid for some operations (for example, load panics if given Ordering::Release). Combining several atomic operations is not automatically atomic as a whole. |
Yes |
The costs differ too. Cell<T> has the same in-memory representation as T. RefCell<T> stores a borrow counter next to the value and checks it on every borrow and borrow_mut. Locks and atomics use synchronization instructions and may involve the operating system when a thread has to wait. How much any of this matters depends on the program, so measure before choosing a type for speed.
The main point of the table is the "what can go wrong" column. Cell cannot fail because it never lends out a reference. RefCell can lend references, and in exchange it can fail at run time. That is a design trade-off, not a defect.
UnsafeCell, the primitive underneath
Normally the compiler assumes that data reached through a &T does not change while the reference is in use, and changing it anyway (for example by converting the &T to a &mut T) is undefined behavior. UnsafeCell<T> is the standard library's core primitive for opting out of that assumption: data inside an UnsafeCell may be changed even while shared references to the UnsafeCell exist.
UnsafeCell itself provides no safety checks. Its get method returns a raw pointer *mut T, and code that writes through that pointer must use unsafe and must itself guarantee that no conflicting access happens. The standard library documentation states that all other types that allow internal mutability, such as Cell<T> and RefCell<T>, use UnsafeCell internally to wrap their data. Each one adds a safe API around it that enforces its own access rule. If you write your own interior-mutability type, you will build it on UnsafeCell or on one of the existing cell types.
Why Rc needs it
Rc<T> lets several owners share one value. Any number of Rc handles may point to the same value, so Rc cannot promise exclusive access through an arbitrary handle. It implements Deref, which gives &T, but not DerefMut, which would give &mut T:
use std::rc::Rc;
fn main() {
let v = Rc::new(vec![1i64]);
v.push(2);
}
error[E0596]: cannot borrow data in an `Rc` as mutable
= help: trait `DerefMut` is required to modify through a dereference,
but it is not implemented for `Rc<Vec<i64>>`
(Rc::get_mut does return &mut T, but only when no other Rc or Weak pointer to the value exists, and Rc::make_mut clones the value if it is shared. Neither lets every owner change one shared value.)
To let every owner change the shared value, put a type with interior mutability inside the Rc:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let log = Rc::new(RefCell::new(Vec::new()));
let other = Rc::clone(&log);
other.borrow_mut().push("from other");
log.borrow_mut().push("from log");
assert_eq!(*log.borrow(), ["from other", "from log"]);
}
log and other are two handles to one RefCell. Each borrow_mut() creates a temporary guard that is dropped at the end of its statement, so the two mutable borrows never overlap and neither call panics.
When the standard library suggests using it
The std::cell documentation describes interior mutability as "something of a last resort" compared with ordinary (inherited) mutability, where changing a value requires owning it or holding a &mut to it. If &mut self can express your design, use it. The documentation names three situations where a cell is appropriate.
1. Changing something behind shared ownership. A value inside an Rc or Arc that several owners need to update, such as a list of observers or a tree of nodes that refer to each other.
2. Caching inside a method that is logically read-only. A &self method computes something expensive and remembers the result. Callers see a read-only API, which is accurate because nothing they can observe changes. When the cached value is computed once and never replaced, OnceCell fits better than RefCell<Option<T>>:
use std::cell::OnceCell;
struct Report {
lines: Vec<String>,
total_len: OnceCell<usize>,
}
impl Report {
fn total_len(&self) -> usize {
*self.total_len.get_or_init(|| self.lines.iter().map(|l| l.len()).sum())
}
}
fn main() {
let r = Report { lines: vec!["ab".into(), "cde".into()], total_len: OnceCell::new() };
assert_eq!(r.total_len(), 5);
assert_eq!(r.total_len(), 5); // the second call returns the stored value
}
3. Clone implementations that update bookkeeping. Clone::clone takes &self, so any change it makes must go through a cell. The documentation's example is Rc itself: it keeps its reference counts in Cells, and Rc::clone(&self) increments the strong count through a shared reference.
Check your understanding
A colleague's struct has a RefCell<HashMap<String, u64>> field used as a memo cache inside &self methods. It works. What would you suggest, and what would you not suggest?
Show answer
Hide answer
Do not suggest removing it just because it is a RefCell. A memo table that gains new keys over time is the second situation above, and RefCell is a reasonable tool for it.
What is worth checking is how it can fail. A borrow_mut() on the field panics if any other borrow of the same field is still alive. That usually happens through reentrancy: a method holds a borrow guard and, while it is alive, calls another &self method that borrows the same field. Each method can be correct when tested alone and still panic when one calls the other. Useful questions:
- Is every borrow short, ending before the method calls anything else on
self? - Is a borrow guard ever held across an
.await? Clippy'sawait_holding_refcell_reflint reports this. - Should a conflicting borrow be reported instead of panicking?
try_borrow_mutreturns aResult.
If the cache holds a single value that is computed once, OnceCell replaces it and removes the borrow-tracking panics (its get_or_init still panics if the initializer reenters the same cell). If the cached value is a small Copy value, such as a counter, a flag or a length, Cell replaces it and cannot panic. Prefer the simplest type that fits.
Lints that check interior mutability
Interior mutability in a const. A const is not a variable at a single address. Each place that uses it gets a fresh copy of the value, as a new temporary. So with
use std::sync::atomic::{AtomicUsize, Ordering};
const COUNTER: AtomicUsize = AtomicUsize::new(0);
fn main() {
for _ in 0..100 {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
println!("{}", COUNTER.load(Ordering::Relaxed));
}
each fetch_add increments a temporary copy that is immediately discarded, and the program prints 0. Changing const to static creates one counter at one address, and the program prints 100. In Rust 1.98.1, rustc warns about the fetch_add call by default (const_item_interior_mutations), and Clippy adds declare_interior_mutable_const for the declaration and borrow_interior_mutable_const for each use.
mutable_key_type. A key stored in a HashMap or HashSet must not change its hash or its equality while it is in the collection. The standard library calls such a change a logic error: the behavior is unspecified (lookups may fail to find the entry, for example), though it is not undefined behavior. A key type with interior mutability can be changed through a shared reference while it is stored, so Clippy's mutable_key_type lint flags maps and sets whose key type contains interior mutability. The obvious mistake is already rejected by the type system: Cell and RefCell do not implement Hash, so HashSet<Cell<u32>> cannot be used (insert fails with E0277). The lint matters for your own key types that contain a cell and implement Hash and Eq by hand.
Summary
&Tis shared and&mut Tis exclusive. The borrowing rule is aliasing XOR mutability, and exclusivity is what lets the compiler optimize.- Interior mutability does not switch that rule off. A cell type upholds the same goal by another mechanism, and for
RefCellthe mechanism reports a violation at run time, by panicking (or by returningErrfrom thetry_methods). - Choose the simplest type that fits:
Cellwhen you only move whole values in and out;OnceCellwhen the value is written once;RefCellwhen you need references to the contents and the borrow pattern can only be checked at run time.Cell,RefCellandOnceCellare notSync; across threads, useMutex,RwLock, atomics orOnceLock. - All of these types are built on
UnsafeCell, the primitive that permits changing data behind a shared reference. - The standard library calls interior mutability "something of a last resort". If
&mut selfexpresses your design, use&mut self.