Scope of a variable with mutable and immutable references

Greets to all!
I am bit confuse about the scope of variable,
let mut s = String::from("hello");

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

How r1 and r2's scope is ended? ? ? As we can access and print them after printing the line r3

when using immutable variables after the mutable causes error, why?
What happened to scope of r1 and r2

The compiler sees that you don't access r1 and r2 later, so it can end their scope earlier. If you accessed them after printing r3 then the compiler wouldn't be able to end their scope earlier, and that would result in a compile error.

2 Likes

Aside: This behavior is "only" about 3 years old. You may still occasionally run across old learning materials from before it was implemented.

1 Like

What happened to the scope is that the compiler is clever enough to see that you don't use the references later.

1 Like

Got it! Thanks for those kind/rapid response

There is a difference between syntactical scope of a variable and whether you can actually still use that variable in Rust. Syntactically, r1 and r2 stay “in scope” until the end of the block. This notion of scope is the one you need to have when determining what which name refers to. There’s for example rules at play that prioritize the innermost variable with the same name when resolving names, a concept also known as “shadowing”.

On the other hand there’s the borrow checker which can restrict certain variable usages further. You could try to put these rules in words as: when you create a new shared reference &s all previous murable references to s may no longer be used. And when you create a new mutable reference &mut s all previous references to s, shared or mutable, may no longer be used.

The “timeframe” (or syntactically roughtly “the part of your code”) in which a reference can be used is called its “lifetime”. Then the rule becomes that the lifetimes of mutable references may not overlap the lifetime of any other reference to the same variable. The compiler is able to figure out the lifetime of a variable automatically, so in your example it determines that the lifetime of r1 and r2 end before r3 is created and its lifetime starts. If the compiler is not able to find any appropriate way to assign lifetimes to your references, you will get a compile error.

For example

if we consider

/* 1 */fn main() {
/* 2 */    let mut s = String::from("hello");
/* 3 */
/* 4 */    let r1 = &s; // no problem
/* 5 */    let r2 = &s; // no problem
/* 6 */    println!("{} and {}", r1, r2);
/* 7 */
/* 8 */    let r3 = &mut s; // no problem
/* 9 */    println!("{}", r3);
/*10 */    
/*11 */    println!("{}", r1);
/*12 */}

Then r1 is created in line 4 and used in line 6 and 11. So its lifetime (the code section in which it can be used) must be at least lines 4 to 11. But this includes line 8 where r3 is created, a mutable reference to the same variable. The lifetimes of r1 and r3 may not overlap, but there’s no way to avoid any overlap, hence the compiler error:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:8:14
   |
4  |     let r1 = &s; // no problem
   |              -- immutable borrow occurs here
...
8  |     let r3 = &mut s; // no problem
   |              ^^^^^^ mutable borrow occurs here
...
11 |     println!("{}", r1);
   |                    -- immutable borrow later used here

error: aborting due to previous error

The compiler points out line 11 to tell you “see, r1 must live at least until line 11”. And it points to line 8 since that’s where r3 is created.


Usually there’s a rule in Rust that variables are only dropped at the end of their scope. This might seem to contradict the way how references are more short-lived than their full scope. The thing to keep in mind here is that references don’t have any destructors, so even though the references may still be on the stack, it can be declared dead (as in, no longer alive) before the end of its scope. Some more advanced example of this kind of phenomenon might me this:

fn main() {
    let v: Vec<i32> = vec![1,2,3];
    let x: &i32 = &v[1];
    let v2: Vec<&i32> = vec![x];
    // the reference at v2[0] points into v, to v[1]
    drop(v);
    // v2 implicitly dropped only at the end of the scope (as indicated below)

    // right here, v2 is not dropped yet, while v is dropped
    // v2 actually still contains the - now dangling - pointer
    // into v. This is safe since the destructor of `Vec<&i32>` does not
    // actually dereference the references it containts (some thing that
    // the borrow checker knows about because of some special unsafe annotations
    // in the standard library)

    // long story short, accessing
    v2[0];
    // here is a compile error, but without this line, this code compiles

    // v2 dropped here
}

Finally, there’s other ways next to borrowing how variables can become unusable in certain ways. For example if you move out of a variable, that variable is still in scope, but you cannot access the (non-existent) value in it anymore. In a more complicated setting, it might actually be the case that you only conditionally moved out of a variable. The variable is still in scope though, and in both cases you can still assign to it.
E.g.


fn main() {
    let mut s = String::from("hi");
    if false { // The type checker or borrow checker don’t care what
               // the value of the condition here is, the code compiles.
               // with `if false` if and only if it compiles with `if true`
        drop(s); // move out of `s`, conditionally
    }
    // s still in scope, i.e.
    // if I type the name "s" here, the compiler will know I mean
    // the s above. Yet i cannot do
    
    // println!("{}", s); // uncomment to get compiler error

    // the reason being: if the condition in the `if` above was true,
    // s would not contain any (valid) value anymore at this point
    
    // I can still do
    s = String::from("hello");
    // though. The old string was still in `s` (since the `if false` statement
    // does nothing in practice) and the string "hi" was only dropped
    // the moment the new one was assigned to `s`.
    
    // now we can
    println!("{}", s);
}
4 Likes