Search code examples
variablesrustreferenceimmutabilityborrow-checker

Why can I use a mutable variable such that its lifetime overlaps with an immutable reference, but I can't use a mutable reference in the same way?


I don't understand why the borrow checker allows a mutable variable's lifetime to overlap with an immutable reference's lifetime, but does not allow a mutable reference's lifetime to overlap with an immutable reference's lifetime.

this compiles:

let mut s = String::from("hello"); // some warning about s not needing to be mutable
let r = &s;
println!("{}, {}", r, s);

but this does not:

let mut s = String::from("hello");
let r_mut = &mut s; // mutable borrow here
let r = &s; // immutable borrow here
println!("{}, and {}", r, r_mut); // mutable borrow used here, error

Why can I use a mutable variable in an immutable way such that its lifetime overlaps with an immutable reference, but I can't use a mutable reference in the same immutable way?


Solution

  • Let's first correct the terminology a bit.

    Variable bindings don't have a lifetime. They have a scope. When a variable binding goes out of scope, the object it binds to will be dropped. This behaviour is not controlled by the borrow checker. Scopes are lexical, and easy to see when looking at the source code.

    Borrows have lifetimes. These lifetimes will be inferred by the borrow checker, and they are not tied to the lexical structure of the code ("non-lexical lifetimes"), though they have been in the past. Lifetimes roughly extend from the point where the borrow is created to the point where it is last used.

    When you create a borrow of a variable, the variable is marked as borrowed by the borrow checker. During the lifetime of the borrow, as inferred by the borrow checker, you can't mutate or move the borrowed variable, regardless of whether you declared the variable binding as mutable or not. If you created a shared borrow, you are allowed to create further shared borrows. Mutable borrows are exclusive, though – a variable that is mutably borrowed can't be used in any other way during the lifetime of the borrow.

    These rules ensure that only one "handle" to a variable – either a binding or a mutable borrow – can be used to mutate a variable at any given time. This is the fundamental invariant Rust ensures. The fact that the scope of a mutable variable binding can overlap with the lifetime of a borrow does not change this, since you can't modify the variable via its binding during the lifetime of the borrow.

    Strictly speaking, there is no such thing as a "mutable variable"; there are only mutable variable bindings. If you have ownership of an object, you can always bind mutably to it to modify it:

    let s = "Hello".to_owned();
    let mut s = s;
    s.push_str(", world!");
    

    There is one more subtlety to your code: println!() is a macro, not a function call. It expands to some code that only borrows the arguments you pass to println!(). So while it looks like you are passing ownership of s to println!(), you actually don't. If you use a macro taking ownership instead, the code stops compiling:

    let mut s = String::from("hello");
    let r = &s;
    dbg!(r, s);
    

    resulting in the error

    error[E0505]: cannot move out of `s` because it is borrowed
     --> src/main.rs:4:5
      |
    3 |     let r = &s;
      |             -- borrow of `s` occurs here
    4 |     dbg!(r, s);
      |     ^^^^^^^^^^^
      |     |
      |     move out of `s` occurs here
      |     borrow later used here
      |
      = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
    

    See also: