We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership II: Borrowing and the Borrow Checker step 18 of 24
Two-phase borrows: why v.push(v.len()) compiles
pub fn build_ledger(n: i64) -> Vec<i64>
Build a vector with these four steps, in this order:
- Start with an empty
Vec<i64>. - Repeat
ntimes: push the vector's current length. Ifn <= 0, do nothing in this step. - Push the current length once more.
- Overwrite the last element with the current length.
n = 0 -> [1]
n = 1 -> [0, 2]
n = 2 -> [0, 1, 3]
n = 5 -> [0, 1, 2, 3, 4, 6]
n = -3 -> [1]
Worked example for n = 2: step 2 gives [0, 1], step 3 pushes the length 2
and gives [0, 1, 2], and step 4 replaces the last element with the length,
which is now 3, giving [0, 1, 3]. In general, for k = max(n, 0) the result
is 0, 1, ..., k - 1 followed by k + 1, so it always has k + 1 elements.
Lengths are usize values, so convert them with as i64 before storing them.
Every test uses a small n, so the vector size is never a concern.
The starter performs these steps on three lines marked (A), (B) and (C). Line
(A) compiles. Lines (B) and (C) are each rejected with error E0502 ("cannot
borrow v as immutable because it is also borrowed as mutable"). The point of
the exercise is to understand why (A) is accepted, then fix (B) and (C)
without changing what the function returns.
The puzzle
Rust's basic borrowing rule says that while a mutable (exclusive) borrow
&mut v is in use, no other borrow of v may be used. See
aliasing XOR mutability
for that rule. Now look at this line:
v.push(v.len());
Vec::push takes &mut self, so the call needs a mutable borrow of v.
v.len() takes &self, so it needs a shared borrow of v. Read naively, the
two borrows overlap, yet the line compiles. The mechanism that allows it is
called a two-phase borrow.
Two-phase borrows
Two-phase borrows were proposed in RFC 2025 and are implemented in the
borrow checker. Before they existed, v.push(v.len()) was rejected.
A method call evaluates its receiver first, then its arguments, then makes
the call. When the receiver needs &mut self, the compiler inserts the
&mut v itself; this is called an autoref. A two-phase borrow splits that
inserted &mut v into two phases:
- Reservation. The borrow is created when the receiver is evaluated, but
until it is used it is treated like a shared borrow. Other code may read
vduring this phase. Writing tovor borrowing it mutably is still an error. - Activation. When the borrow is first used, here when
pushis actually called, it becomes an ordinary exclusive borrow. Any shared borrow taken during the reservation must have ended by then.
The rustc developer guide explains this with a desugaring like the one below.
It is pseudo-code: &two_phase is not real syntax.
let tmp = &two_phase v; // reservation: acts like a shared borrow
let n = v.len(); // a read during the reservation is allowed
Vec::push(tmp, n); // activation: tmp is now exclusive
The shared borrow inside v.len() ends when len returns, before activation,
so nothing overlaps with the exclusive use. Two examples show the limits of
the reservation phase:
// E0499: the argument writes to `v` during the reservation.
v.push({ v.push(1); 2 });
// E0502: `r` is a shared borrow that is still used after the call,
// so it is alive when the mutable borrow activates.
let r = &v;
v.push(r.len());
println!("{}", r.len());
Where two-phase borrows apply
The rustc developer guide lists three places where the compiler creates a two-phase borrow:
- the autoref for a
&mut selfmethod call, as inv.push(v.len()); - an implicit reborrow of a
&mutvariable passed as a function argument, as ing(r, r.len())wherer: &mut Vec<i64>andgtakes&mut Vec<i64>first; - the implicit borrow of the left operand in an overloaded compound
assignment such as
w += w.get(), wherew's type implementsAddAssign.
Borrows created anywhere else are ordinary borrows. This explains both failing lines in the starter.
- (B) An
&mutwritten in the source is not two-phase.Vec::push(&mut v, v.len() as i64)calls the same function asv.push(v.len() as i64), but the&mut vis written by hand rather than inserted by an autoref, so it is exclusive from the moment it is created. The argumentv.len()then readsvwhile that exclusive borrow is alive: E0502. The same applies to writingg(&mut *r, r.len())instead ofg(r, r.len()). - (C) Overloaded indexing is not two-phase. For a
Vec,v[i] = xcallsIndexMut::index_mut(&mut v, i)and writes through the returned reference. Inv[v.len() - 1] = ..., the mutable borrow forindex_mutis taken before the indexv.len() - 1is computed, and indexing is not one of the three places listed above, so the read in the index is E0502. Built-in indexing of arrays and slices does not go throughIndexMut, andv[v.len() - 1] = 7compiles whenvis an array or a&mut [i64].
One similar-looking line does compile: v[0] = v.len() as i64;. This has
nothing to do with two-phase borrows. In an assignment place = value, Rust
evaluates the right-hand value first and the left-hand place second, so
v.len() has finished before index_mut borrows v. In line (C) the read of
v is inside the place itself, so evaluation order does not help.
How to fix it
Two-phase borrows explain why some lines compile. They are not a rule to design code around. The Rust Reference does not specify them in detail, so the compiler's documentation is the main description of the current behavior.
When a line is rejected like this, compute the value into a local variable
first. A usize result such as v.len() does not keep v borrowed, so the
shared borrow ends before the mutable one begins. For line (C) the compiler's
E0502 diagnostic suggests the same approach ("try adding a local storing
this", pointing at v.len()). Hoisting the whole index works too:
let last = v.len() - 1;
v[last] = v.len() as i64;
Line (B) can be fixed the same way, or by writing it with method-call syntax.
Your submission must compile, pass the tests and pass
cargo clippy -- -D warnings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.