During the last month I have been studying Rust, and I have started to appreciate the power of its memory safety constraints.
Rules:
- You cannot have more than one mutable borrows to the same value with overlapping lifetimes.
- You cannot have mutable and immutable borrows to the same value with overlapping lifetimes.
- You cannot move out a value from a borrowed context.
Goal:
- at any given point in time, you can only have one usable reference to modify a particular value.
- equivalent to: at any given point in time you cannot have 2 valid references that modify the same variable.
- forbid dangling references.
The compiler does the best but sometimes it is too strict. NLL (non-lexical lifetimes) has improved the flexibility of the compiler to be more permissive but the problem is still not completely solved.
Mental model to understand how the compiler works:
-
A mutable reference to the value:
let r = &mut v
, invalidate all the previous references tov
. -
A mutable reference can transfer temporary the ability to modify the value it refer to. That ability will be given back as soon as the original reference is used again and the revorrow invalidated.
Given:
let r1 = &mut v
.let r2 = &mut v
invalidates the reference:r1
let r2 = &mut (*r1)
does not invalidater1
, becauser1
is reborrowed. This meansr2
is valid as long asr1
is not used later
Reborrows
The idea behind reborrows is that a mutable reference can transfer its power to another variable for a limited amount of time.
For example in the code below:
1: let mut v = vec![0];
2: let r = &mut v;
3: let r1 = &mut *r; // reborrows
4: r1.push(1);
5: r.push(2);
r1
is reborrowed from r
and it is valid as long as r
is not used. It is crucial to note that reborrows do not invalidate the previous references to the same variable.
However, a reborrowed reference cannot be used after the original reference is used again.
Tthe code below does not compile because r1
reborrows temporarily from r
but it is invalidated at line: 4 and cannot be used at line: 5.
1: let mut v = vec![0];
2: let r = &mut v;
3: let r1 = &mut *r; // reborrows
4: r.push(1);
5: r1.push(2);
Reborrows are useful for mutable references as function parameters because mutable references do not implement the Copy
trait. This means that the mutable reference is no longer valid after it is passed as a parameter in the function.
Fortunately, mutable references are automatically reborrowed as function parameters. This is the reason why the following code works:
1: let mut v = vec![0];
2: let r = &mut v;
4: r.push(1); // r is reborrowed as &mut (*r) and not moved
5: r.push(2); // r is not moved by the previous call
Reborrows explain why the following code works even if we have 2 mutable references to the same variable:
1: let mut v = vec![0];
2: let r1 = &mut v;
3: let r2 = &mut (*r1) // r2 reborrows from r1 and it is valid as long as r1 is not modified
4: r2.push_back(0); // a new reborrows is implicitly created from r2 and it does not invalidate anything
5: r1.push_back(0); // r1 has not been invalidated since it has given temporary power to r2
// from this point we can no longer use r2 because it has been invalidated by the previous line that claims back the power previously given to r1
However, reborrows invalidate other previous reborrows to the same variable, following the classical rule of having only one mutable reference at a time.
This is why the code below does not work.
1: let mut v = vec![0];
2: let r = &mut v;
3: let r1 = &mut *r; // r1 reborrows r
4: r.push(1); // r is reborrowed as &mut (*r) invalidating all previous references to (*r) -> r1
5: r1.push(2); // error: r1 has been invalidated by the previous call
2-Phase Borrows And Mutable References:
-
let mut v = vec![1]; let r = &mut v; r.push(r.len());
Without the 2 phase borrows the code above will not compile because the outer call (
push()
) creates a reborrow ofr
:&mut *r
, and the inner call another borrow of the reserved value.With 2-phase borrows, the first reborrow is converted to
&mut2 *r
and later activated when the second borrow is out of scope. -
let mut v = vec![1]; let r = &mut v; r.push(v.len());
Even with the 2-phase borrows it does not compile.
The inner call, causes an inplicitly borrow of
v
that clashes withr
-
let mut v = vec![1]; let r = &mut v; r.push({r.push(0);0});
Even with the 2-phase borrows it does not compile.
The inner call requires a
&mut2
reborrows of*r
that is not allowed by the 2-phase borrows since the outer call already created a&mut2
reborrows of*r
.
References:
Have a nice day 🚀