Why Does Rust Prevent Multiple Mutable References even if they are in different Scopes?

I’m experimenting with RUST's Rust’s borrowing rules, particularly regarding mutable references in nested scopes.

I ran into this confusion:

Code A

fn main() {
    let mut s = String::from("hello");
    let r3 = &mut s;
    {
        let r4 = &mut s;
        println!("r4 is {}", r4);
    }
    println!("r3 is {}", r3);
}

This code results in a compile-time error due to overlapping mutable references, but the following CODE B works fine:

CODE B

fn main() {
    let mut s = String::from("hello");
    {
        let r4 = &mut s;
        println!("r4 is {}", r4);
    }
    let r3 = &mut s;
    println!("r3 is {}", r3);
}

My understanding was that - Although I cannot have multiple mutable references, but if they are in different scope, they should be fine.

However, it seems Code B only works because r3 is declared and used without r4 coming in ( into different scopes in between).

Questions:

  1. Why does it matter if r4 is declared between the declaration and usage of Mutable Reference 'r3'?
  2. Can someone clarify how Rust enforces these borrowing rules in such a nested scenario, and why?
1 Like

This would violate the non-aliasing (exclusivity) rule of mutable references in case of overlapping scopes, as you have shown in your first snippet. The r3 mutable reference to s is used in the print statement after the scope in which r4 exists, so under the current borrow checker rules it must be live from the point of declaration to the point where we use it in the print statement. This liveness scope includes the block in which we declare r4. Due to the exclusivity of the mutable borrow in r3, the borrow checker prevents us from creating a second mutable borrow to s while the first one is still live.

The nll RFC describes this a lot better than I could.

As a practical suggestion, reborrowing would work to avoid the two simultaneous mutable references.

2 Likes

This wouldn't be fine. Example:

    let mut v = vec![7];
    let a = &v;
    let b = &a[0];
    {
        let c = &mut v;
        c.pop();
    }
    println!("{b}");

This would try to print a non-existent number. It is prevented by disallowing multiple references to v.

9 Likes

In your snippet A, they are not in "different scopes". The scope of r4 overlaps with that of r3, because the former is a subset of the latter.

You need to understand that the rules of mutable exclusivity are not arbitrary. They are not designed to make you go mad; they are designed to ensure memory safety, which means you must not be able to mutate the same place through two distinct references in an interleaving manner.

If your code snippet "A" were allowed, then in the innermost block, you would have both r3 and r4 in scope, and both of them would be usable (for mutating s). This is what differentiates this example from reborrowing r3 (which is allowed).

4 Likes

Note that it can be allowed via reborrowing: let r4 = &mut *r3, but this explicitly prevents use of r3 for the duration of the reborrow. When references are taken independently, the borrow checker can't prove they won't break exclusivity rules.

8 Likes

thanks man. I guess I need to brush up my mental models on "liveness" a bit

Here's a modification of Code B where I remove the inner scope. It still compiles.

    let mut s = String::from("hello");

    let r4 = &mut s;
    println!("r4 is {}", r4);

    let r3 = &mut s;
    println!("r3 is {}", r3);

The way that scopes interact with borrow checking is that when a variable goes out of scope

  • its destructor, if it has one, may require access to the value
  • the variable becomes uninitialized

The only thing that went out of scope in your example was r4. References do not have destructors so the first bullet doesn't apply. You didn't have a reference to r4 that would be invalid when r4 became uninitialized, so the second bullet didn't matter.

References to references are rare, so scopes rarely matter for references.

Both versions of Code B work because the borrow of r4 is not needed after the first println!, so it expires before r3 is created. Scope doesn't enter into this analysis at all.


Instead of writing up what I just did, I could have modified Code A and showed that it still does not compile, and talk about how the scope doesn't matter to that example at all either. The error is because r4 is created between r3 being created and r3 being used, so both exclusive references would have to exist at the same time.

Or more generally, you used s in an exclusive manner (taking a &mut) when another borrow of s (r3) was still alive. Moving s at that location would produce a similar error, for example, because moving is also an exclusive use.


In broad strokes, the borrow checker

  • Analyzes the liveness of Rust lifetimes (those '_ things) based on lifetime annotations[1] and how places[2] are used[3]
  • Analyzes when and how[4] places are borrowed[5]
  • Looks at every use of every place to check for conflicts between the use and live borrows

The interaction with lexical scopes is that they add a use of everything that goes out of scope. This can be a deep use like a destructor (which may examine everything about the value going out of scope), or a shallow use like when a reference goes out of scope (a no-op besides making the reference uninitialized).

Many newcomers try to influence borrows/lifetimes by adding scopes, but it rarely works out since lifetimes are non-lexical, so they're typically just adding more uses. Probably the main case where it's beneficial is when you change where the desctructor of something like a mutex lock runs (which could also be accomplished with a drop(..)).


  1. constraints like 'a: 'b ↩︎

  2. such as variables, but also say *r3, which is where reborrowing comes in ↩︎

  3. roughly a place that is alive and has a type with a lifetime means the lifetime is alive ↩︎

  4. shared, exclusive ↩︎

  5. this is a refinement of the last step because a place borrowed by a reference with lifetime 'x, say, may be borrowed for less than 'x if the reference is overwritten, for example ↩︎

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.