We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 17 of 24
Reborrowing: why &mut sometimes moves and sometimes doesn't
Two functions and a pinned helper you must not change.
// PINNED — do not change this signature or body.
fn consume<T>(t: T) -> T { t }
pub fn tally(slots: &mut [i64], ops: &[i64]) -> i64
pub fn nudge(slot: Option<&mut i64>, d: i64) -> i64
tally applies each op: op number i targets slots[i % slots.len()], adds
the op’s value to it, and then floors that slot at 0 (so it never goes
negative). Every mutation must be routed through consume — twice, once for
the add and once for the floor. It returns the sum of the slots afterwards. An
empty slots does nothing and returns 0.
nudge does the same two steps to a single optional slot: add d, then floor
at 0. It returns the slot’s final value, or 0 if there was no slot.
slots [1,1], ops [3,4,5] -> slots [9,5], total 14
slots [10], ops [-3,-4] -> slots [3], total 3
slots [2,2,2], ops [-9] -> slots [0,2,2], total 4 <- floored at 0
Neither function compiles. Both fail with the same error, and it is one of the most confusing messages in Rust:
error[E0382]: use of moved value: `r`
r is a &mut i64. A reference got moved. If your model of references is
“they are little pointers, pointers are cheap, cheap things are Copy“, this
message is nonsense.
The largest piece of invisible machinery in the language
&T is Copy. Any number of shared references may exist, so duplicating one
is harmless.
&mut T is not Copy, and cannot be — the whole point is that exactly
one exists. So passing a &mut to a function moves it, and using it again is
E0382. That is consistent and it follows directly from item 3.3.
Except that this obviously works:
fn double(r: &mut i64) { *r *= 2; }
let mut n = 1;
let r = &mut n;
double(r);
double(r); // ...moved value? No. Compiles fine.
So sometimes &mut moves and sometimes it does not, with no visible
difference at the call site. Until you know why, &mut appears to obey
inconsistent rules, and you cannot predict which programs will compile.
The mechanism is reborrowing. When the compiler already knows a reference
type is expected, it does not pass your &mut — it silently inserts
&mut *r, creating a new, shorter exclusive borrow derived from yours. Your
original is frozen for the duration of the reborrow and usable again
afterwards. double(r) is really double(&mut *r).
The rule, stated precisely
Two halves:
-
A reborrow may reduce permissions, never increase them. From a
&mut Tyou may take a&mut Tor a&T. From a&Tyou may take only a&T. - The reborrow’s region must be strictly inside the original’s. The parent is unusable while the child is live, and usable again once it dies.
And the part that catches everyone — implicit reborrowing happens only when the compiler already knows a reference type is expected. Concretely, it does not happen for:
-
a generic parameter:
fn take<T>(t: T).Tis not known to be a reference, so nothing is inserted, and your&mutis moved. This isconsume, and it is why it is pinned. - storing into a struct field or any other non-coercion site.
-
Option<&mut T>and other references inside another type. The expected type isOption<_>, not&mut _, so no reborrow is inserted and matching on theOptionmoves the reference out. - across a closure capture.
In every one of those cases you write the reborrow yourself: &mut *r.
The two fixes
For tally: consume(r) moves. consume(&mut *r) reborrows, so r survives
and the second call works. That is the entire change — five characters, twice.
For nudge: if let Some(x) = slot moves the &mut i64 out of the Option,
because Option<&mut i64> is not Copy either. What you want is a fresh
Option holding a reborrow, which is exactly what Option::as_deref_mut
produces: Option<&mut i64> in, Option<&mut i64> out, with the inner
reference reborrowed rather than moved. as_mut() gives you Option<&mut &mut i64> which also works but is uglier.
This is why Option<&mut T> has a reputation. It is not that it is broken; it
is that the implicit reborrow you have unknowingly depended on for months
stops being inserted, and there is no visible reason why.
Being honest about the state of this
The Rust Reference does not fully specify reborrowing
(rust-lang/reference#788 has been open for years). It is a real, load-bearing
language feature with no normative description — the behaviour is defined by
what rustc does. haibane_tenshi’s well-known critique makes the sharper claim
that reborrowing “cannot be expressed in the type system” as it stands: there
is no trait, no signature, no way for user code to opt in or to be generic
over it. That is why Option<&mut T> and generic parameters have the gap they
do.
There is a 2026 project goal — Reborrow and CoerceShared traits — to make
it a first-class, user-implementable operation. It is not shipped and you
cannot use it. But it means “why is reborrowing so weird” has an answer better
than “it just is”: it was never designed, it accreted, and the language team
agrees it needs fixing.
About the pinned helper
consume<T>(t: T) -> T must stay generic and must keep its body. If you
change it to fn consume(t: &mut i64) -> &mut i64, the compiler suddenly
knows a reference is expected, inserts the reborrow for you, and the whole
lesson evaporates. Leave it alone.
Watch for needless_option_as_deref (a real lint that fires when as_deref
really is a no-op — it will not fire on the correct answer here),
explicit_deref_methods and borrow_deref_ref (&*x where x would do).
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.