Skip to content

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

Medium Primitives

What a &mut parameter actually promises

pub fn merge_into(dst: &mut Vec<i64>, src: &[i64])
pub fn self_merge(xs: &mut Vec<i64>) -> usize

merge_into appends every element of src to dst and then sorts dst ascending. It is written for you and is correct.

self_merge must merge a vector into itself: the result contains every original element twice, sorted ascending. It returns the new length.

dst [3,1], src [2]  ->  merge_into gives [1,2,3]
                        self_merge then gives [1,1,2,2,3,3], len 6
dst [5],   src []   ->  [5]      then [5,5], len 2
dst [],    src []   ->  []       then [],   len 0

The obvious implementation of self_merge is one line:

merge_into(xs, xs);

It does not compile, and understanding why closes the loop on this whole track.

The promise

error[E0502]: cannot borrow `*xs` as immutable because it is also borrowed as mutable

When you write dst: &mut Vec<i64> you are not saying “I might write to this”. You are making a promise on behalf of every caller:

For as long as this call runs, dst is reachable through this reference and nothing else. No other name, no other reference, no other thread can read it or write it.

merge_into(xs, xs) breaks that promise on its face: src would alias dst. So the compiler rejects the call site — not the function.

And this is not a compiler limitation to be worked around. It is the entire value proposition. merge_into‘s body reads src while pushing to dst. If they aliased, dst.extend_from_slice(src) would be reading a buffer that is being reallocated underneath it. In C++ this is a real bug you have to think about at every call site; in Rust the signature makes it impossible.

The corollary in machine code

rustc lowers &mut parameters to LLVM’s noalias attribute — the same thing C’s restrict means, except that in C you promise it by hand and in Rust the type system proves it.

So the optimiser is entitled to assume no aliasing: it can keep *dst in a register across a call, skip a reload after a store through an unrelated pointer, and collapse branches that could only differ if two pointers were equal. The guarantee is consumed. It is not a style rule you might reasonably break if you were careful — it has already been spent by codegen, and being careful is not a substitute. Item 3.23 works through the assembly.

Three legal routes, and what each costs

  1. extend_from_within(..) — a Vec method for exactly this: copy a range of the vector onto its own end. One &mut, no second borrow, no temporary. The copying happens inside Vec, where the buffer’s owner can reason about its own reallocation. This is the right answer.
  2. Copy the source out first. Build an owned snapshot, then merge that in. Correct, and an extra allocation you did not need. (.clone() is withdrawn in this track anyway.)
  3. split_at_mut — split the vector in two provably-disjoint halves and work across them (item 3.16). Overkill here, but the general shape when the two regions really are distinct parts of the same buffer.

All three share the same insight: you cannot hand the same place to a &mut and a & at once, so make one of them not be that place.

The elision trap, planted now so it does not ambush you later

While you are here, look at this signature and predict what it does to the caller:

fn get(&mut self) -> &Thing

Elision fills in one lifetime for both, so this is really fn get<'a>(&'a mut self) -> &'a Thing. The returned reference is immutable, which makes it look harmless — but its lifetime is tied to the &mut self borrow, so self stays mutably borrowed for as long as the returned &Thing lives. Hold it in a local and the whole object is frozen until you drop it.

That is the single most surprising consequence of lifetime elision, and it catches people who have been writing Rust for a year. Track 8 covers the rules properly; for now, just recognise the shape.

The reverse shape is worse and clippy denies it outright:

fn f(x: &T) -> &mut U      // error: clippy::mut_from_ref

mut_from_ref is a correctness lint and — uniquely in this whole cluster — it is deny-by-default, not warn-by-default. Producing an exclusive reference out of a shared one lets a caller make two &muts to the same place with no unsafe in sight, which is unsound. It is not hypothetical: a bug of this shape in the standard library caused the Rust 1.15.1 point release.

Two more lints in the neighbourhood: needless_pass_by_ref_mut (you asked for &mut and never wrote through it) and unnecessary_mut_passed (you passed &mut x to something that wanted &x).

About the test cases

The harness asserts the vector after merge_into and before self_merge, as well as after. Without that intermediate assertion, an implementation that gets the append order wrong would be hidden by the final sort.

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

Loading visualization…