Skip to content

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

Medium Primitives

E0499: cannot borrow as mutable more than once

pub fn apply_swaps(xs: &mut [i64], swaps: &[[usize; 2]]) -> usize

Apply each [i, j] pair in order, exchanging xs[i] and xs[j]. If either index is out of range, skip that pair. Return how many pairs were skipped.

[1,2,3], [[0,2]]  -> [3,2,1], skipped 0
[1,2,3], [[1,1]]  -> [1,2,3], skipped 0   <- i == j is a NO-OP, not a skip
[1,2,3], [[0,5]]  -> [1,2,3], skipped 1
[],      [[0,0]]  -> [],      skipped 1

Read the second line again, because it is the interesting one and there is a test for it. A pair like [1, 1] is in range. Swapping an element with itself changes nothing and is not an error.

The starter, and why it cannot work

let a = &mut xs[i];
let b = &mut xs[j];    // error[E0499]: cannot borrow `xs[_]` as mutable
std::mem::swap(a, b);  //              more than once at a time

You know from item 3.3 why: xs[i] goes through IndexMut, which takes &mut xs — the whole slice — and hands back a reference derived from it. Two such calls are two overlapping exclusive loans of the same place. The index values are runtime data; the compiler has no theory of arithmetic that would let it conclude i != j, and in this program it cannot, because sometimes i == j really is true.

This is the first item where the answer is not “restructure”

Up to now, every fix has been “rearrange your own code”: copy the value out, end the loan earlier, compute then mutate. That does not work here. There is no arrangement of &mut xs[i] and &mut xs[j] that the checker will accept, because the thing you want it to believe is genuinely not derivable from the types in front of it.

So the move is different, and it is the move the entire expert half of Rust is built on:

When the checker cannot construct the proof, find the API whose signature already contains it.

Somebody has done this work. They wrote a function whose type says “these references are disjoint”, discharged the obligation once inside it, and published it. You do not need to be clever; you need to know the catalogue.

get_disjoint_mut

fn get_disjoint_mut<const N: usize>(&mut self, indices: [usize; N])
    -> Result<[&mut T; N], GetDisjointMutError>

Stabilised in Rust 1.86 (it spent its unstable life called get_many_mut). You hand it an array of indices; it hands you back an array of exclusive references, or an error. That single Result is the whole lesson: the runtime check is the proof obligation the compiler could not discharge. It did not vanish; somebody moved it from compile time to run time and made you handle the failure.

And look at the error type. It has exactly two variants:

  • IndexOutOfBounds
  • OverlappingIndices

Those are precisely the two things the compiler could not rule out. The API is a specification of what the borrow checker was worried about. Rust is full of this shape once you start looking.

For this problem, OverlappingIndices is the i == j case — so Err here means “nothing to do”, not “something went wrong”. Do not count it as a skip.

There is also a get_disjoint_unchecked_mut. It is unsafe, it skips the check, and it is not to be used here — #![forbid(unsafe_code)] is the standing rule from this track through Track 16, and “I was sure the indices differed” is exactly the reasoning that makes C memory-unsafe.

The one-liner you should also know

Slices have had swap since forever:

xs.swap(i, j);

It takes two indices, panics if either is out of range, and explicitly allows i == j. It is the right answer for this specific job, and if you write the three-line temporary-variable swap by hand, clippy will fire manual_swap at you. (Get the order of the three lines wrong and you get almost_swapped as well — a double punishment for a very old bug.)

So why introduce get_disjoint_mut at all? Because slice::swap is a special case — it works because it is one function that owns both accesses. The moment you need to do something to two elements that is not exchange them, swap is no help and get_disjoint_mut is. Learn the general tool on the problem where the specific tool also happens to exist.

Whichever route you take, be careful with for i in 0..swaps.len() — clippy’s needless_range_loop will send you to for s in swaps.

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

Loading visualization…