Disclaimer: not quite satisfied with the answers so far. I think a more in-depth answer would be more valuable.
Ownership & Borrowing
The two cornerstones concept of the handling of values (and references) in Rust are ownership and borrowing:
- Ownership can be transferred: the value is moved from one variable to another.
- Ownership can be "lent out" temporarily: a reference (mutable or not) to the value is created, borrowing it.
It is also possible to clone (or copy) a value, in which case a new value is created, independent from the original. In this case, no transfer of ownership occurs, and the borrow of the value that is cloned (or copies) ends by the time the new value is available.
Copying References
Immutable (shared) references are copyable (they implement the Copy
trait), since you can have any number of immutable references to a given value at any moment in time.
Mutable (unique) references are NOT, however, as otherwise, they'd wouldn't be unique.
This means that assigning a reference to a variable will have different properties, depending on whether it's immutable or not:
let x = 3;
let y = &x;
let z = y; // z is a Copy of y
let mut a = 7;
let b = &mut a;
let c = b; // b is _moved_ to c, the variable b cannot be used afterwards.
The latter is a key motivation for being able to temporarily borrow a reference (or mutable reference) of a
from b
, for example, to make a function call:
fn foo(r: &mut i32) {
*r += 1;
}
fn main() {
let mut a = 7;
let b = &mut a;
foo(b);
println!("{b}"); // Expected error, `b` was moved out.
}
Enter Re-borrowing
The solution to the above issue is the idea of re-borrowing, which is borrowing the referred value out of its reference, temporarily.
The key idea is that it works very similarly to borrowing from a value. When you borrow from a value:
- The variable you borrow from is intact.
- The variable is inaccessible -- in case of mutable borrowing -- for the lifetime of the borrow (a subset of the lifetime of the reference).
And thus re-borrowing allows the same key idea:
- The reference you borrow from is intact.
- The reference you borrow from is inaccessible -- in cast of mutable borrowing -- for the lifetime of the borrow.
The syntax of re-borrowing is a bit special. You can't write &reference
as that would create a reference to the reference, which is valid, but not quite what you want. Thus you are left writing &*reference
to indicate that you want a reference to what reference
refers to itself.
Due to this syntax being quite a bit verbose, and not too ergonomics, re-borrowing is usually done automatically for you. Taking our previous example:
fn foo(r: &mut i32) {
*r += 1; // Re-borrow 1
}
fn main() {
let mut a = 7;
let b = &mut a;
foo(b); // Re-borrow 2
println!("{b}");
}
Starting from the bottom, when a function taking a mutable reference is called with a mutable reference, the compiler automatically inserts &mut *
, so that in fact the example does not lead to a compiler error even though b
cannot be copied, only moved.
Similarly, the *r += 1
is actually re-borrowing. This time the *
is inserted manually, and the compiler then applies the &mut
implicitly as it rewrites the expression to (*r).add_assign(1)
where add_assign
takes &mut self
.
Re-borrowing is everywhere, it's just so automatic you probably never noticed.
Applying re-borrowing
Let's examine your examples to figure out what's going on:
fn main() {
let mut foo = 1;
let mut borrower = &mut foo;
let rr = &mut borrower; // (1)
let borrower2 = &mut (**rr); // (2)
*borrower2 = 2; // (3)
*borrower = 3; // (4)
println!("{}", foo);
}
In order:
- Creates a mutable reference to a mutable reference to
foo
:
rr
is of type &mut &mut i32
.
borrower
is now borrowed.
- Create a mutable reference to
foo
:
borrower2
is of type &mut i32
.
rr
is now reborrowed.
- Assign to the value referred to by
borrower2
.
- Assign to the value referred to by
borrower
.
Why is (4) allowed?
The short answer is that the lifetime of the borrow of rr
by borrower2
ended, and the lifetime of the borrow of borrower
by rr
ended, and thus borrower
is no longer borrowed.
The long answer is that originally the Rust compiler would consider a variable borrowed for the lifetime of its reference, which was typically until the end of the scope in which the reference was created. This was inflexible, and required introducing extra scopes:
fn main() {
let mut foo = 1;
let mut borrower = &mut foo;
{
let rr = &mut borrower; // (1)
let borrower2 = &mut (**rr); // (2)
*borrower2 = 2; // (3)
}
*borrower = 3; // (4)
println!("{}", foo);
}
To make our life easier, Non-Lexical Lifetimes (NLL) were implemented, giving the latitude to the compiler to end borrows earlier than the end of the scope of the references that created the borrow. In general, this means that the borrow only lasts until the last use of the reference.
And therefore your example works without the (manual) extra scope.
What if you reverse the assignments? That is:
fn main() {
let mut foo = 1;
let mut borrower = &mut foo;
let rr = &mut borrower; // (1)
let borrower2 = &mut (**rr); // (2)
*borrower = 3; // (A)
*borrower2 = 2; // (B)
println!("{}", foo);
}
Well, then NLL doesn't save you any longer, since it only contracts the tail of the borrow, not its head. It could theoretically only borrow from first to last use, rather than from creation to last use, but it doesn't, and therefore:
borrower2
is last used at (B).
- Thus,
rr
must be borrowed from (2) to (B).
- Thus,
borrower
must be borrowed from (1) to (B).
- Thus,
borrower
is borrowed at (A) -- and it's an error to try to re-borrow it again.
All clear?