Skip to content

← Ownership II: Borrowing and the Borrow Checker step 17 of 24

Hard Primitives

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:

  1. A reborrow may reduce permissions, never increase them. From a &mut T you may take a &mut T or a &T. From a &T you may take only a &T.
  2. 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). T is not known to be a reference, so nothing is inserted, and your &mut is moved. This is consume, 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 is Option<_>, not &mut _, so no reborrow is inserted and matching on the Option moves 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…