Need some clarification on the behavior of mutable references

This is my first day learning Rust. Currently on Chapter 4 of The Book where I came across something that I don't understand and it bothers me.

fn main() {
     let mut s = String::from("foo")
     let r1 = &mut s;
     let r2 = &mut s;
}

I tried to build this code fully expecting it to fail because, as stated in the book, we aren't supposed to have more than one mutable reference to the same value at any point in time:

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value.

However, I was surprised to find that this code will compile as long as we don't use/refer to r1 after r2's declaration. My question is, why? Why does the compiler allow this? Since r1 can never be referenced or used again after r2's declaration, why not just prevent the code from compiling to begin with?

My own attempts and experiments to find the answer:

I modified the code into something like this.

fn main() {
    let mut s = String::from("foo");
    let r1 = &mut s;
    println!("This one should work {r1}");
    {
       println!("{r1} can still be used here."); 

        // Declare another mutable reference in new scope
       let r2 = &mut s;

       // Referencing r1 past this point will most likely fail
       println!("Use {r2} here. Should work fine.");
    } // r2 goes out of scope.
    
    // Is r1 still valid after r2 went out of scope?
    println!("Test whether {r1} is still valid.");

    // Result: r1 is no longer valid.

    // Declare another reference
    let r3 = &mut s;
    println!("{r3}"); // still okay.

r1 could no longer be used even though r2 had gone out of scope. If I think of it in terms of borrowing, this certainly makes sense. There can only be one "mutable borrower" for s at any given point in time. So if r2 is was borrowing mutably from s, r1 cannot also be borrowing mutably from s at the same time because of that rule.

After r2 went out of scope, nobody was borrowing mutably from s anymore. So a new entity (r3) was allowed to borrow mutably from s again.

In the above scenario, I'm guessing that r1 was invalidated the moment r2 was created regardless of the fact that they were in different scopes. If that's really the case, doesn't it make allowing things like:

let r1 = &mut s;
let r2 = &mut s;

all the more pointless?

Why wait until r1 was referred to before a compilation error is returned? Why not prevent compilation from the get-go?

Any insight is much appreciated.

Your code looks pointless because you are doing nothing with r1, but if you did actually use it then it becomes useful to not have to wrap it in its own scope.

The borrow checker used to work like you expected though, but it was clear it was too limiting. Eventually it was changed to allow code like this with the so called "Non-Lexical Lifetimes"

7 Likes

You can use r1 before r2 declaration:
I made this change to your code and it is allowed.

fn main() {
    let mut s = String::from("foo");
    let r1 = &mut s;
    r1.push_str("!!");
    let r2 = &mut s;
    r2.push_str("!!");
}

Rust sees it something like this (generated using cargo asm --dev --mir from crates.io: Rust Package Registry)

    let mut _0: ();
    let mut _1: std::string::String;
    let _3: ();
    let _4: &str;
    let _6: ();
    let _7: &str;
// skip
    bb0: {
        _1 = <String as From<&str>>::from(const "foo") -> [return: bb1, unwind continue];
    }

    bb1: { // scope for r1
        _2 = &mut _1;
        _4 = const "!!";
        _3 = String::push_str(_2, _4) -> [return: bb2, unwind: bb5];
    }

    bb2: { // scope for r2
        _5 = &mut _1;
        _7 = const "!!";
        _6 = String::push_str(_5, _7) -> [return: bb3, unwind: bb5];
    }

As you noticed r1 stops being valid after you create r2.

Next I changed the code to look like something you would normally write:

fn main() {
    let mut s = String::from("foo");
    s.push_str("!!");
    s.push_str("!!");
}

And looks like the compiler sees it more or less the same way:

    let mut _0: ();
    let mut _1: std::string::String;
    let _2: ();
    let mut _3: &mut std::string::String;
    let _4: &str;
    let _5: ();
    let mut _6: &mut std::string::String;
    let _7: &str;
    scope 1 {
        debug s => _1;
    }

    bb0: {
        _1 = <String as From<&str>>::from(const "foo") -> [return: bb1, unwind continue];
    }

    bb1: {
        _3 = &mut _1;
        _4 = const "!!";
        _2 = String::push_str(move _3, _4) -> [return: bb2, unwind: bb5];
    }

    bb2: {
        _6 = &mut _1;
        _7 = const "!!";
        _5 = String::push_str(move _6, _7) -> [return: bb3, unwind: bb5];
    }

Thanks for the link.

After spending a bit more time thinking about it, I think I can see now why it will be too limiting. Especially when function calls are taken into account. For example:

fn main() {
    let mut s = String::from("foo");
    let r = &mut s;
    // Do something with the string through r
    
    // At some point, the string needs to be processed by another function
    modify_string(&mut s); // This would fail if the compiler didn't allow multiple mutable reference declarations
}

fn modify_string(s: &mut String) {
      // Do something with string
}

If the compiler doesn't allow multiple mutable reference declarations like these:

let r1 = &mut s;
let r2 = &mut s;

Then the compiler also won't allow us to use &mut s as an argument to a function because that's essentially the same (at least according to my limited understanding) as assigning a mutable reference to another variable (the function parameter).

When I started using Rust in earnest, NLL was announced but not implemented. My old code was littered with stuff like

{ // NLL Begin

// ...

} // NLL END

Since you're thinking about it, I'll point out another mechanism around &mut which introductory material[1] tend not to mention -- reborrows. In this code, two &mut to s exist at the same time...

    let mut s = String::from("foo");
    let r1 = &mut s;
    let r2 = &mut *r1;
    r2.push_str("!!");
    println!("{r1}");

...but r2 is a reborrow of *r1. The compiler recognizes this, and r1 is effectively "inactive" so long as r2 is alive. (If you try to use r2 again after using r1, you'll get an error.)

So this demonstrates that "no two &mut at one time" is a bit oversimplified. It's more like "no two &mut active at one time".


  1. and even official docs ↩ī¸Ž

3 Likes