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

Repair two functions that reuse mutable references. Keep consume unchanged:

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

Required behavior

tally processes operations in order. Operation i targets slots[i % slots.len()]: add the operation's value, then replace a negative result with 0. For each operation, pass a mutable reference through consume twice: once for the addition and once for the clamp to zero. Return the sum of all slots after processing the operations.

Empty slots return 0 without processing operations. Slots that receive no operation stay unchanged, including any initial negative values. With no operations, return the sum of the original slots. Inputs keep additions and intermediate sums within the i64 range.

Initial slots Operations Final slots Returned total
[1, 1] [3, 4, 5] [9, 5] 14
[10] [-3, -4] [3] 3
[2, 2, 2] [-9] [0, 2, 2] 4

nudge adds d to its optional slot, clamps the result to zero and returns the final value. For None, it does nothing and returns 0. Keep its two update steps separate to practice borrowing from the same Option twice. This function does not need to call consume.

The harness first calls tally, saves its total and resulting slots, then calls nudge on the first slot. Its output contains total, after_tally, nudged and the final slots after both functions.

Why the starter moves a reference

Shared references (&T) implement Copy; mutable references (&mut T) do not. Moving a mutable reference transfers that reference, so its old binding cannot be read afterward. This is what happens in consume(r) when the generic type T is inferred from r.

A reborrow creates a reference through an existing reference. The expression &mut *r borrows the value reached through r for a new mutable access. That access must fit within the original borrow's lifetime; it need not always be strictly shorter. While the mutable reborrow is still needed, conflicting use through the original reference is disallowed. After its last use, the original can be used again.

When Rust inserts a reborrow

A parameter with a known mutable-reference type provides enough context:

fn double(r: &mut i64) { *r *= 2; }

fn main() {
    let mut n = 1;
    let r = &mut n;
    double(r);
    double(r);
    assert_eq!(n, 4);
}

Each call can reborrow r, as if written double(&mut *r). By contrast, consume(r) infers T from the argument and moves the reference. Writing consume(&mut *r) makes the intended temporary access explicit.

Generic functions do not always prevent reborrowing: explicitly supplying a reference type, as in consume::<&mut i64>(r), also provides type context. Struct fields can provide that context too:

struct Slot<'a> { value: &'a mut i64 }

fn main() {
    let mut n = 1;
    let r = &mut n;
    let slot = Slot { value: r };
    *slot.value += 1;
    *r += 1;
    assert_eq!(n, 3);
}

Here the field use finishes before r is used again. If a later use of slot.value still needed the reborrow, that overlapping access through r would be rejected. Look at the expected type and later uses rather than treating all function calls or struct initializers alike.

Reusing the optional reference

In the starter, if let Some(r) = slot moves the contained &mut i64 into r. A second match cannot move it out again. Option<&mut i64> does not implement Copy, and Rust does not automatically rebuild that wrapper around a reborrow.

slot.as_deref_mut() borrows the option mutably and returns a new Option<&mut i64> borrowing the value reached through its contained reference. Match that temporary option for each update. The original option remains available once each borrow ends, including for the final map_or call. The binding must be mutable to call as_deref_mut().

as_mut() is another option, but it returns Option<&mut &mut i64> here, so updating the integer requires one more dereference.

Complete the repair

Use explicit reborrows for the two consume calls in tally, and borrow from the option for the two updates in nudge. Keep the generic helper's signature and body: changing it to a concrete mutable-reference function would bypass the part of the exercise that demonstrates type inference.

Check both returned values and mutations, then address Clippy warnings. Passing requires compilation, correct results and the lint gate.