Yes, that’s indeed the case. And interestingly enough, it’s not always been this way. Lifetimes used to be more “lexical”, i.e. focusing on the lexical scope a variable is declared in. If you take your code to an older version on Compile Explorer, you can indeed find an error message
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> <source>:5:5
|
4 | let t: &mut String = &mut s;
| - first mutable borrow occurs here
5 | s.push('?');
| ^ second mutable borrow occurs here
...
8 | }
| - first borrow ends here
error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
--> <source>:6:20
|
4 | let t: &mut String = &mut s;
| - mutable borrow occurs here
5 | s.push('?');
6 | println!("{}", s);
| ^ immutable borrow occurs here
7 |
8 | }
| - mutable borrow ends here
error: aborting due to 2 previous errors
And to fix it, you’d have to write
let mut s = String::from("Does work");
{
let t: &mut String = &mut s;
// …can use `t` here
}
s.push('?');
println!("{}", s);
The newer system, often called “non-lexical lifetimes” is more clever than this though. For types like &mut String
for which the compiler knows they cannot possibly don’t do anything to the reference target when being dropped, it’s now the last usage that’s important to determine how long s
is borrowed by t
; and when t
is implicitly dropped at the end of its scope, it’s allowed to be already dead / pointing to invalid memory / etc.
Only if you add a usage of t
at a later point, you get an error. E.g.
let mut s = String::from("Does work");
let t: &mut String = &mut s;
s.push('?');
// and now use `t` again
let _ = &t;
println!("{}", s);
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> <source>:5:1
|
4 | let t: &mut String = &mut s;
| ------ first mutable borrow occurs here
5 | s.push('?');
| ^ second mutable borrow occurs here
6 | // and now use `t` again
7 | let _ = &t;
| -- first borrow later used here
error: aborting due to previous error
This improvement to the borrow checker means a significant improvement in usability. It’s hard to imagine nowadays the amount of additional struggle when programming with Rust that came from the need to add extra blocks or otherwise limit the scope of variables, just to make the borrow checker happy with code that wasn’t “really” any problematic at all. I cannot imagine it either since when I’ve learned Rust, we already had the new borrow checking stuff (which was introduced in about 2018, roughly at the same time as the first newer edition, I believe).
Even nowadays, people still manage to run into borrow checker limitations regularly, and some of them are even already improved upon in the next WIP upgrade called “polonius”.