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

swaps is a list of index pairs [i, j]. Process the pairs in the order given. For each pair:

  • If i or j (or both) is not a valid index into xs, that is, if it is greater than or equal to xs.len(), leave xs unchanged and count the pair as skipped.
  • Otherwise exchange the values at xs[i] and xs[j].

Return the number of skipped pairs. The function changes xs in place; the test harness prints the final xs and the returned count. Each swap sees the result of the swaps before it, so the same pair applied twice undoes itself. An empty swaps list returns 0 and leaves xs alone. Any usize value can appear as an index, including very large ones; they are simply out of range. The values in xs are only moved, never added or compared, so overflow cannot happen.

xs        swaps     final xs   skipped
[1,2,3]   [[0,2]]   [3,2,1]    0
[1,2,3]   [[1,1]]   [1,2,3]    0   (i == j: in range, so not skipped)
[1,2,3]   [[0,5]]   [1,2,3]    1   (5 is out of range)
[]        [[0,0]]   []         1   (an empty slice has no valid index)

The second row needs care, and a test checks it. A pair such as [1, 1] uses a valid index twice. Swapping an element with itself changes nothing, but it is not a skip.

Why the starter code does not compile

The starter checks the bounds and then tries to take two mutable references:

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

A mutable reference (&mut T) is an exclusive borrow: while it is in use, no other reference may reach the same data. Error E0499 means the compiler found two mutable borrows that might overlap and are both still in use.

For a slice, xs[i] is built into the language rather than a call to a trait method. The xs[_] in the error message is how the compiler names "some element of xs": the borrow checker does not track index values when it decides whether two borrows overlap. It treats any two element borrows of the same slice as possibly the same element. That holds even for constant indices, so &mut xs[0] and &mut xs[1] together are rejected with the same error. (Two different struct fields, such as &mut p.a and &mut p.b, are accepted, because field names are known at compile time.)

In this exercise the compiler is right to be cautious: i == j really does happen, and two live &mut references to the same element are exactly what Rust forbids. Moving the two lines around does not help. What does help is a standard library function that checks the indices at run time and only then gives you references it knows are separate.

get_disjoint_mut

Slices have a method get_disjoint_mut, stable since Rust 1.86 (it was called get_many_mut while unstable). Simplified to plain usize indices, its use looks like this:

match xs.get_disjoint_mut([i, j]) {
    Ok([a, b]) => std::mem::swap(a, b), // a and b point at different elements
    Err(e) => { /* e is a GetDisjointMutError */ }
}

You give it an array of indices and get back either an array of mutable references, one per index, or an error. (The real signature is generic: it also accepts arrays of ranges, which return mutable sub-slices.) The error type, std::slice::GetDisjointMutError, has two variants:

  • IndexOutOfBounds: at least one index is not less than the slice length.
  • OverlappingIndices: two of the requested positions overlap. For single indices this means two of them are equal.

These are the two conditions the compiler could not rule out on its own. The method performs those checks at run time, and its Result makes the caller decide what to do when they fail. "Disjoint" means "sharing no elements".

In this problem the bounds are checked first, so an Err after that point can only be OverlappingIndices, which means i == j. That pair is a no-op: do nothing, and do not count it as skipped.

There is also get_disjoint_unchecked_mut, an unsafe method that skips the checks and leaves it to the caller to guarantee them. Calling it with equal or out-of-range indices is undefined behavior. Use the checked method here.

The shorter route: slice::swap

Slices have had a swap method since Rust 1.0:

xs.swap(i, j);

It panics if either index is out of range and leaves the slice unchanged when i == j. Because you check the bounds before calling it, it never panics in this exercise, and it is the simplest correct answer.

If you instead write the swap by hand with a temporary (let t = xs[i]; xs[i] = xs[j]; xs[j] = t;), it compiles, because each line borrows xs only briefly and copies an i64. Clippy reports it as manual_swap and suggests xs.swap(i, j). If you forget the temporary and write xs[i] = xs[j]; xs[j] = xs[i];, both elements end up with the old value of xs[j], and Clippy reports almost_swapped.

swap works because a single method performs both accesses inside itself. When you need to do something other than exchange two elements, for example add one element's value to another through two references, swap does not apply and get_disjoint_mut does. Either approach is accepted here.

Grading

Your code must compile, pass the tests, and pass cargo clippy -- -D warnings (every Clippy warning is treated as an error). One lint to watch for: a loop written as for k in 0..swaps.len() that only uses k to index swaps triggers needless_range_loop. Loop over the pairs directly with for s in swaps (or for &[i, j] in swaps).