Skip to content

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

Medium Primitives

iter_mut(): mutating every element without ever holding two &mut

Two in-place functions, applied in that order by the harness.

pub fn running_max_in_place(xs: &mut [i64])
pub fn zip_add(dst: &mut [i64], src: &[i64])

running_max_in_place replaces each element with the maximum of itself and everything before it. [3,2,1] becomes [3,3,3]; [1,2,3] is unchanged.

zip_add adds src[i] to dst[i] for as far as both slices go. If they differ in length, the extra elements of the longer one are simply not used — in either direction. dst=[5,5], src=[1] gives [6,5]; dst=[1], src=[2,3] gives [3]. Both directions are tested.

The specification says in place. A scan-based one-liner that builds a new vector is elegant and is not what is being asked for; there are cases pinning the in-place behaviour.

The starter does not compile.

The thing that looks like it should be illegal

for x in xs.iter_mut() {
    *x *= 2;
}

Stop and consider how strange this is under the rule you learned in item 3.3.

xs.iter_mut() takes &mut xs — an exclusive loan of the whole slice — and hands you an iterator that holds it. Then next() gives you a &mut i64 pointing into that slice. So now there are two exclusive references to overlapping memory: the iterator’s, and yours. That is the exact thing E0499 exists to forbid. Why is this allowed?

Beginners either never notice, or notice and conclude that iter_mut is magic they should not imitate. Both are bad outcomes, because the pattern is the answer to about a third of all beginner borrow errors.

Why it is sound

The answer is in Iterator‘s definition:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Look at what is missing. Item is an associated type with no lifetime parameter, and no connection to the &mut self in next. For IterMut<'a, T>, Item = &'a mut T — tied to 'a, the lifetime of the original slice, not to the borrow of the iterator.

That is the whole trick. The reference next() returns outlives the borrow of the iterator that produced it. The iterator is free again the instant next() returns, so there is only ever one live exclusive reference to any given element at any time, and elements never overlap because the iterator walks forward and never revisits.

The Nomicon says it directly: this is why iter_mut can be written safely at all, and it is also why the “lending iterator” (an iterator whose items borrow from the iterator itself, so only one can exist at a time) is a genuinely different and much harder thing that Rust only gained the machinery for recently.

The practical takeaway: iter_mut is not an exception to the rule. It is a demonstration that the rule is about overlapping live references, and a forward-only walk never produces two.

Carrying state across the loop

running_max_in_place needs a value that survives from one element to the next. That is fine — a plain local:

let mut best = i64::MIN;
for x in xs.iter_mut() {
    best = best.max(*x);
    *x = best;
}

best is an i64, not a reference, so it has nothing to do with the borrow of the slice. Read through *x, write through *x, keep the accumulator outside. This shape covers running sums, running maxima, prefix products, deltas, and most of what you would otherwise reach for scan to do — with no allocation.

i64::MIN as the seed is correct here: any real element is >= it, and the empty slice never enters the loop.

Why the starter fails

let prev = &mut xs[i - 1];
let cur = &mut xs[i];        // error[E0499]

Two exclusive loans of the same slice, live at the same time — item 3.7’s error, in the shape people most often reach for when they need “this element and the one before it”. iter_mut plus an accumulator dissolves it, because you never need prev as a reference: you only need its value, and a local holds that perfectly.

When you genuinely need two references into one slice at once — neighbours, halves, arbitrary index pairs — that is items 3.7 and 3.16: get_disjoint_mut, split_at_mut, chunks_mut, windows. Not this one.

zip truncates

for (d, s) in dst.iter_mut().zip(src) {
    *d += *s;
}

Iterator::zip stops when either side stops. That is exactly the “whichever is shorter” rule the spec asks for, in one call, with no length comparison and no bounds check. Write it with indices and you get either a panic or a needless_range_loop from clippy, and probably both.

(manual_slice_fill is a related lint: a loop writing the same constant into every element should be slice::fill.)

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…