🎓 A1Learner
🔍

Rust Two-Phase Borrows

A two-phase borrow is a method receiver borrow that starts in two steps. The &mut borrow is reserved first.

The line that should not work — but does

push needs &mut v. len needs &v. One line asks for both, and the compiler says yes:

let mut v = vec![1, 2, 3];
v.push(v.len());
println!("{:?}", v);

Phase one: reserve. Phase two: activate

Rust settles the receiver borrow in two steps. First it reserves the &mut — a promise, not yet active. The arguments are read while the promise is pending. Only then does the borrow switch on, and the call runs:

c.add(c.get()); // get() is read while the &mut for add is only reserved

Only method receivers get the two phases

Write the same shape as a free function and it fails. There is no receiver, so the &mut argument is active from the first moment, and reading v[0] collides with it:

fn push_again(v: &mut Vec<i32>, item: i32) { v.push(item); }
push_again(&mut v, v[0]); // error E0502: v borrowed as mutable, then as immutable

It works for any method pair

insert borrows the Vec mutably, and its position argument reads the same Vec — same two phases, same result:

let mut v = vec![1, 2, 3];
v.insert(0, v.len() as i32);
println!("{:?}", v);

Example

struct Counter { n: i32 }

impl Counter {
    fn add(&mut self, m: i32) {
        self.n += m;
    }
    fn get(&self) -> i32 {
        self.n
    }
}

fn main() {
    let mut v = vec![1, 2, 3];
    v.push(v.len());
    println!("{:?}", v);

    let mut c = Counter { n: 5 };
    c.add(c.get());
    println!("n = {}", c.n);
}

Key points

  • Two-phase borrow: the &mut a method call takes on its receiver comes in two phases.
  • First it is reserved.
  • Then it is activated
  • While the borrow is only reserved, the call's arguments are worked out.
  • They may borrow the same value in a shared way
  • Use it without thinking: v.push(v.len()), v.insert(0, v.len()) and thing.add(thing.get()) all just work
  • The gift covers only the automatic &mut a method call takes on its receiver.
  • An explicit &mut handed over as a plain function argument is different.
  • It is fully active from the start

In this example, two two-phase calls run. First, v.push(v.len()): the &mut that push takes on v is reserved. len then reads the length 3 under a shared borrow. The &mut activates, and 3 is pushed. The line prints [1, 2, 3, 3]. Below, Counter gives the same shape a name: add borrows c mutably, get borrows it shared, and c.add(c.get()) doubles n from 5 to 10. Neither call needed a temporary variable, because the receiver borrow waited politely while the argument was read.

Note

Two-phase borrows arrived in the same era as non-lexical lifetimes, and for the same reason.

Real code kept tripping over borrows that looked tangled but were perfectly safe.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Why does v.push(v.len()) compile, while push_again(&mut v, v[0]) does not?