Reputation: 658
After reading about Rust's scopes and references, I wrote a simple code to test them.
fn main() {
// 1. define a string
let mut a = String::from("great");
// 2. get a mutable reference
let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);
// 3. A scope: c is useless after exiting this scope
{
let c = &a;
println!("c = {:?}", c);
}
// 4. Use the mutable reference as the immutable reference's scope
// is no longer valid.
println!("b = {:?}", b); // <- why does this line cause an error?
}
As far as I understand:
In 3
, c
is created within a scope, and no mutables are used in it. Hence,
when c
goes out of scope, it is clear that c
will not be used anymore (as
it would be invalid) and hence b
, a mutable reference, can be used safely in
4
.
The expected output:
b = "great breeze"
c = "great breeze"
b = "great breeze"
Rust produces the following error:
error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
--> src/main.rs:12:17
|
6 | let b = &mut a;
| ------ mutable borrow occurs here
...
12 | let c = &a;
| ^^ immutable borrow occurs here
...
18 | println!("b = {:?}", b); // <- why does this line cause an error?
| - mutable borrow later used here
(This is just what I think is happening, and can be a fallacy)
It seems that no matter what, you cannot use any mutable references (b
)
once an immutable reference (c
) was made (or "seen" by the rust
compiler), whether in a scope or not.
This is much like:
At any given time, you can have either one mutable reference or any number of immutable references.
This situation is similar to:
let mut a = String::from("great");
let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);
// ^ b's "session" ends here. Using 'b' below will violate the rule:
// "one mutable at a time"
// can use multiple immutables: follows the rule
// "Can have multiple immutables at one time"
let c = &a;
let d = &a;
println!("c = {:?}, d = {:?}", c, d);
println!("b = {:?}", b); // !!! Error
Also, as long as we are using immutable references, the original object or reference becomes "immutable". As said in Rust Book:
We also cannot have a mutable reference while we have an immutable one. Users of an immutable reference don’t expect the values to suddenly change out from under them!
and
...you can have either one mutable reference...
let mut a = String::from("great");
let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);
let c = &b; // b cannot be changed as long as c is in use
b.push_str(" summer"); // <- ERROR: as b has already been borrowed.
println!("c = {:?}", c); // <- immutable borrow is used here
So this code above somewhat explains @Shepmaster's solution.
Going back to the original code and removing the scope:
// 1. define a string
let mut a = String::from("great");
// 2. get a mutable reference
let b = &mut a;
b.push_str(" breeze");
println!("b = {:?}", b);
// 3. No scopes here.
let c = &a;
println!("c = {:?}", c);
// 4. Use the mutable reference as the immutable reference's scope
// is no longer valid.
println!("b = {:?}", b); // <- why does this line cause an error?
Now it is clear why this code has an error. The rust compiler sees that
we are using a mutable b
(which is a mutable reference of a
,
therefore a
becomes immutable) while also borrowing an immutable
reference c
. I like to call it "no immutables in between".
Or we can also call it "un-sandwiching". You cannot have/use a mutable between "immutable declaration" and "immutable use" and vice-versa.
But this still does not answer the question of why scopes fail here.
c
into a scope, why does the Rust
compiler produce this error message?Upvotes: 1
Views: 1153
Reputation: 154866
Your question is why doesn't compiler allow c
to refer to data which is already mutably borrowed. I for one would expect that to be disallowed to begin with!
But - when you comment out the very last println!()
, the code compiles correctly. Presumably that's what led you to conclude that aliasing is allowed, "as long as mutables aren't in the same scope". I argue that that conclusion is incorrect, and here is why.
While it's true that there are some cases where aliasing is allowed for references in sub-scopes, it requires further restrictions, such as narrowing an existing reference through struct projection. (E.g. given a let r = &mut point
, you can write let rx = &mut r.x
, i.e. temporarily mutably borrow a subset of mutably borrowed data.) But that's not the case here. Here c
is a completely new shared reference to data already mutably referenced by b
. That should never be allowed, and yet it compiles.
The answer lies with the compiler's analysis of non-lexical lifetimes (NLL). When you comment out the last println!()
, the compiler notices that:
b
isn't Drop
, so no one can observe a difference if we pretend it was dropped sooner, perhaps immediately after last use.
b
is no longer used after the first println!()
.
So NLL inserts an invisible drop(b)
after the first println!()
, thereby allowing introduction of c
in the first place. It's only because of the implicit drop(b)
that c
doesn't create a mutable alias. In other words, the scope of b
is artificially shortened from what would be determined by purely lexical analysis (its position relative to {
and }
), hence non-lexical lifetime.
You can test this hypothesis by wrapping the reference in a newtype. For example, this is equivalent to your code, and it still compiles with the last println!()
commented out:
#[derive(Debug)]
struct Ref<'a>(&'a mut String);
fn main() {
let mut a = String::from("great");
let b = Ref(&mut a);
b.0.push_str(" breeze");
println!("b = {:?}", b);
{
let c = &a;
println!("c = {:?}", c);
}
//println!("b = {:?}", b);
}
But, if we merely implement Drop
for Ref
, the code no longer compiles:
// causes compilation error for code above
impl Drop for Ref<'_> {
fn drop(&mut self) {
}
}
To explicitly answer your question:
Even after explicitly moving
c
into a scope, why does the Rust compiler produce this error message?
Because c
is not allowed to exist alongside b
to begin with, regardless of being an an inner scope. When it is allowed to exist is in cases where the compiler can prove that b
is never used in parallel with c
and it's safe to drop it before c
is even constructed. In that case aliasing is "allowed" because there's no actual aliasing despite b
"being in scope" - on the level of the generated MIR/HIR, it's only c
that refers to the data.
Upvotes: 4