We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 15 of 24
`UnsafeCell`: the only legal path to interior mutability
pub fn memo_fib(queries: Vec<u64>) -> Vec<u64>
Answer a list of Fibonacci queries with a memo table. The cache lives in a
struct Memo { cache: UnsafeCell<Vec<u64>> }, and the lookup method takes
&self — not &mut self. Queries arrive in any order, repeat freely, and
never exceed 90 (so every answer fits in a u64). fib(0) == 0,
fib(1) == 1.
The starter contains the helper everybody writes first, and it is rejected by a deny-by-default clippy lint. That rejection is the lesson.
The one construct that retracts a language guarantee
Normally &T promises the pointee will not change while the reference is
live. That promise is not a convention; the optimiser acts on it, caching
loads across calls and reordering freely.
UnsafeCell<T> is the single language-level construct that retracts it.
It is a #[repr(transparent)] wrapper the compiler knows about specially, and
it is the foundation of every interior-mutability type in std: Cell,
RefCell, Mutex, RwLock, AtomicUsize, OnceCell. All of them are
UnsafeCell plus a bookkeeping strategy.
Three rules, stated exactly:
-
It relaxes only the shared-reference immutability guarantee. The
&mutuniqueness guarantee is completely untouched — there is no legal way to obtain two live&mutto the same place,UnsafeCellor not. -
The only valid way to get a
*mut Tto the contents of a sharedUnsafeCell<T>is.get()or.raw_get(). Casting&UnsafeCell<T>to*const Tand then to*mut Tis not the same thing and is not sanctioned. -
It does nothing about data races. Concurrent conflicting access still
needs atomics or a lock.
UnsafeCell<T>is!Sync, which is exactly whyCellandRefCellare single-threaded types.
::: question What does rustc say if you try the cast route —
&x as *const T as *mut T — and write through it?
It refuses, by name:
error: assigning to `&T` is undefined behavior, consider using an `UnsafeCell`
That is invalid_reference_casting, and it is deny-by-default. rustc grew
the lint because the cast is the obvious-looking workaround and it is always
wrong: the immutability guarantee attaches to the &T, and no amount of
casting removes a guarantee that was already made.
Note what the diagnostic does: it names the correct tool. UnsafeCell is not
a loophole you found; it is the sanctioned mechanism, and using it is how you
tell the compiler “do not assume this place is frozen” — which it then does
not.
:::
Why fn cache_mut(&self) -> &mut Vec<u64> is rejected
It is the first thing everybody writes, and clippy’s mut_from_ref is
correctness/deny and stops it:
error: mutable borrow from immutable input(s)
The lint is not being fussy about style. Think about what that signature
promises. It says: given any &Memo — of which there may be arbitrarily many,
simultaneously — hand back a &mut Vec<u64>. Call it twice and you have two
live &mut to the same place, which is undefined behaviour, produced by
entirely safe code, with no unsafe at either call site. The function is
unsound: not “risky”, not “needs care” — a bug in the API regardless of
how carefully anyone calls it.
The fix is the real lesson: keep the pointer raw, and mint the reference only at the point of use.
fn fib(&self, n: usize) -> u64 {
// SAFETY: ... why no other reference is live right now ...
let cache = unsafe { &mut *self.cache.get() };
...
}
Here the &mut is a local. Its lifetime is bounded by the body. Nothing
escapes, so nothing can alias, and the safety argument is a statement about
this function rather than about every possible caller — which is the only
kind of statement you can actually verify.
::: question self.cache.get() returns *mut Vec<u64> and needs no unsafe.
Why not?
Because obtaining a raw pointer is always safe. The obligation is at the
dereference.
This is the create-versus-deref asymmetry from item 17.5, and it is why the
“keep it raw, mint late” tactic works at all: raw pointers can be stored,
copied, offset and passed around with no aliasing consequences whatsoever. The
aliasing models — Stacked Borrows, Tree Borrows — constrain references.
Every & or &mut you create performs a retag that can invalidate siblings;
a *mut does not.
So the discipline for shared-mutable data structures is: hold raw pointers in
your fields, convert to a reference for the shortest possible window, and never
return a reference whose lifetime you did not tie to a &mut self. Item 17.14
is the full story.
:::
Writing this one
UnsafeCell::new(vec![0, 1]) seeds the table with fib(0) and fib(1). On a
query for n, extend the table until cache.len() > n, then return
cache[n]. Repeat queries hit the table and do no work; out-of-order queries
are automatic, because filling to n fills everything below n too.
fib(90) is 2 880 067 194 370 816 120, comfortably inside u64. The problem
guarantees n <= 90, but note that -O turns off overflow checks, so a
larger n would wrap silently rather than panic — use wrapping_add and be
explicit about it rather than relying on a check that is not there.
Note the impl Default. Clippy’s new_without_default is style/warn, which
under -D warnings is a failure, and it fires on any pub type with a
pub fn new() taking no arguments. Two lines, once.
A note on threads
UnsafeCell<T> is !Sync, so Memo is !Sync, so &Memo cannot cross a
thread boundary. That is the auto-trait system doing exactly the right thing
for free — and it is why this design is sound single-threaded and would be a
data race if you forced it across threads.
Rust has a SyncUnsafeCell for the case where you want the shared-mutability
relaxation and Sync, and have your own synchronisation. It is still
unstable on 1.95, so if you need it you hand-roll it: a newtype around
UnsafeCell<T> with an unsafe impl<T: Send> Sync and a # Safety section
explaining what discipline makes it sound.
What this grader cannot check
If your safety comment is wrong — if there is a path on which two &mut
into the cache are live at once — every test here still passes, because the
aliasing violation is invisible at runtime on this input. The check is your
argument, plus mut_from_ref catching the one shape that is unsound by
construction. Item 17.14 is about the tool that would catch the rest, and why
it cannot run here.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.