Hello all
Sorry if this question is not for this category, please let me know if that's the case.
Need some help understanding the borrow checker while I learn rust.
Why this works without any issues?
fn main() {
let mut s = String::from("Hello");
let r1 = &mut s;
change_string(r1);
let r2 = &mut s;
change_string(r2);
println!("{}", s);
}
fn change_string(some_string: &mut String) {
some_string.push_str(" gordy!!!");
}
And this one, by the contrary, does not compile?
let r1 = &mut s;
let r2 = &mut s;
change_string(r1);
change_string(r2);
println!("{}", s);
}
Is r1 "consumed" somehow or "ignored" in the first case?
Many thanks in advance.
Regards
2 Likes
My naive explanation is:
In the first example, you do an "ordered" use of the two mutable borrows r1 and r2: You create r1, and then use it. After that, you create r2, and use it, but you never use r1 again.
In the second example, you create r1 and r2, and use both to change the string content. This is not allowed in Rust.
The experts will be able to explain it in better words.
[EDIT]
I think it is called Non-Lexical Lifetimes (NLL) introduced in the Rust 2018 edition.
3 Likes
Afaik, the reference lifetime is active until when the last time said reference is used (in same scope)
In your first code
let r1 = &mut s;
change_string(r1); // r1 is active until here
// in this line, r1 is no longer active because the compiler see that after change_string fn (the last user that uses r1), no other user uses it so it drops the reference right after change_string fn is finished
let r2 = &mut s; // so r2 can start mutable borrow safely
change_string(r2);
println!("{}", s);
Now in the 2nd code
let r1 = &mut s; // r1 start mutable reference
let r2 = &mut s; // r2 starts new mutable reference
// assume we ignore the 2 change_string fn below. no error will happen, because no user use the references so their lifetimes are not extended (aka they are dropped again right after the creation, the compiler will remove them completely, optimized out)
// now let's go back to the original code, there are 2 users
change_string(r1); // this user uses r1, so r1 lifetime is extended until the end of this fn, it means the first mutable reference is counted as active when r2 is being created. so let r2 = &mut s triggers the double mutable references error because r1 lifetime is extended beyond the line of r2 creation
change_string(r2);
println!("{}", s);
1 Like
The way I would think of it is
- Uses of
r1 keep the original borrow of s active
- Creating a
&mut s to assign to r2 is incompatible with s being borrowed
// The next line creates an exclusive borrow of `s`.
let r1 = &mut s;
change_string(r1);
// No more uses of `r1`, so the original borrow can end here.
// So `s` is no longer borrowed and the following line is accepted.
let r2 = &mut s;
change_string(r2);
/* L1 */ let r1 = &mut s;
/* L2 */ // The use of `r1` at L8 means `s` is still borrowed at L4.
/* L3 */ // So the following line is not accepted (you get a borrow check error).
/* L4 */ let r2 = &mut s;
/* L5 */ change_string(r1);
/* L6 */ change_string(r2);
/* L7 */
/* L8 */ println!("{}", s);
It may help to know that &mut _ are exclusive references. So long as a &mut to s is active, you can only access s through the &mut.
Lexical scopes don't end borrows. They also don't end reborrows.
The way I think of it is that a reference going out of scope makes the reference itself uninitialized. That's incompatible with the reference itself being borrowed, like if you had a &mut &mut String and the inner reference went out of scope. But a reference going out of scope doesn't access the referent (*r2) and it is compatible with the referent being (re)borrowed.
So if you don't have nested references, a reference going out of scope is pretty meaningless.
4 Likes
What I mean by same scope is the scope of where the variable that is used to store the reference is declared (within same scope, remember in passing reference they are not moved. Only smart pointers that are moved. Reference is only moved if it is assigned to new variable because only 1 &mut is allowed at a time). For example:
let mut a =100;
{
let b = &mut a;
*b = 101;
} // b is dropped at the end of this scope
Where the lifetime is the last time where the reference is used (as long as it is inside the scope of the variable declaration (both the lifetime variable declaration and the original value variable declaration). For example:
// a will not be moved here because the parameter type is &, it will copy the memory address under the hood
// when a is active, b can not mutate the value aka b is disabled but not dropped
fn task1(a: &mut i32) { .... }
// fn task2
// fn task 3
{ // scope a
let mut a = 100;
let mut b = &mut a;
{ // scope b
// scope b is inside scope a, so a and b can be used inside this scope
task1(b);
task2(b);
task3(b); // the lifetime of b is extended until task3, because after task3 no other user within the scope a uses it
let c = &mut a; // so new mutable reference can start here
}
// a is still active here because there is no move happen inside scope and a is i32 which implements copy, if there is move call it will just copy the value and a will still active here
// but b is no longer active here, because after task3 fn call, no new user uses b so b lifetime ends after task3 fn. it is why new mutable ref can be created below task3 fn
} // a is dropped here
println!("{}", *b); // error, b is no longer active here because it is out of the scope, the scope here simply doesn't know what is b, so can't extend the lifetime here. a is also no longer active here
Another examples worth to know are:
let a;
{
let mut b = 100;
a = &mut b;
}
*a = 101; // error dangling, because b is no longer valid
let mut a = 100;
let b = &mut a;
let c = b; // the reference is moved here, because only 1 mutable ref can active at a time
*b = 101; // trigger the error because b is no longer active
1 Like
Omg, thank you very much to all of you 
A lot of think about with your comments to understand the borrow checker ( if ever
)
Thank you very much to all.