Reference Lifetimes?

I realized I have a gap in my understanding of reference lifetimes when this code compiles:

#[derive(Debug)]
struct Mog {
    pub name: String
}

fn main() {
    let mut mog = Mog { name: "Joe".to_string() };
    let another = &mut mog;
    another.name = "Kim".to_string();
    mog.name = "Back Again".to_string();
}

My expectation is that the mutable reference another should exist for the entire scope of the fn main. Is the compiler aware that we never use the mutable borrow another AFTER we modify the owner of Mog? My understanding of reference lifetimes is the following:

fn main() {
    let mut mog = Mog { name: "Joe".to_string() }; // ---------------+-- `b
    let another = &mut mog;                        // ------- +-- `a |
                                                   //         |      |
    another.name = "Kim".to_string();              //         |      |
    mog.name = "Back Again".to_string();           //         |      | 
                                                   // --------+      |
                                                   // ---------------+
}

I expected another to live through the end of fn main, therefore, making mog.name ="Back Again".to_string(); to fail because we still have a life mutable reference.

The below hits a compilation error:

#[derive(Debug)]
struct Mog {
    pub name: String
}

fn main() {
    let mut mog = Mog { name: "Joe".to_string() };
    let another = &mut mog;
    another.name = "Kim".to_string();
    mog.name = "Back Again".to_string();
    another.name = "Switcheroo".to_string();
}

This results in:

error[E0506]: cannot assign to `mog.name` because it is borrowed
  --> src/main.rs:10:5
   |
8  |     let another = &mut mog;
   |                   -------- borrow of `mog.name` occurs here
9  |     another.name = "Kim".to_string();
10 |     mog.name = "Back Again".to_string();
   |     ^^^^^^^^ assignment to borrowed `mog.name` occurs here
11 |     another.name = "Switcheroo".to_string();
   |     ------------ borrow later used here

For more information about this error, try `rustc --explain E0506`.
error: could not compile `playground` due to previous error

Which reveals that I have some misunderstanding about how Rust handles reference lifetimes.

The Rust compiler is smart enough to end lifetimes at their last use instead of at the end of the scope.

Note that it hasn't always been this way. The feature is called non-lexical lifetimes.

4 Likes

Yes, precisely. In older versions of Rust, each lifetime always lasted until the end of a lexical scope (e.g., a closing brace), which matched your expectation. But Rust 1.31 introduced a new feature called non-lexical lifetimes. Now, borrows can end immediately after their last use.

3 Likes

Thanks for the clarification and the link to rfcs!

Note that the ability of the compiler to (soundly) end the lifetime of the &mut Mog before it goes out of scope is related to &mut Mog doing nothing when dropped. In Rust, destructors always run at the end of scope, so in general on would think that might be problematic:

fn main() {
    let mut mog = Mog { name: "Joe".to_string() };
    let another = &mut mog;
    another.name = "Kim".to_string();
    mog.name = "Back Again".to_string();
    // in principal, destructor of `another` runs here; but actually,
    // explicitly calling `drop(another)` here makes the code no longer compile
}

And in fact, if you wrap the reference into another struct, e.g.

#[derive(Debug)]
struct Mog {
    pub name: String
}

struct ContainsReference<'a>(&'a mut Mog);

fn main() {
    let mut mog = Mog { name: "Joe".to_string() };
    let another = ContainsReference(&mut mog);
    another.0.name = "Kim".to_string();
    mog.name = "Back Again".to_string();
}

well… that still works… but if this new struct were to implement Drop it stops working

#[derive(Debug)]
struct Mog {
    pub name: String
}

struct ContainsReference<'a>(&'a mut Mog);
impl Drop for ContainsReference<'_> { fn drop(&mut self) {} }

fn main() {
    let mut mog = Mog { name: "Joe".to_string() };
    let another = ContainsReference(&mut mog);
    another.0.name = "Kim".to_string();
    mog.name = "Back Again".to_string();
}
error[E0506]: cannot assign to `mog.name` because it is borrowed
  --> src/main.rs:13:5
   |
11 |     let another = ContainsReference(&mut mog);
   |                                     -------- borrow of `mog.name` occurs here
12 |     another.0.name = "Kim".to_string();
13 |     mog.name = "Back Again".to_string();
   |     ^^^^^^^^ assignment to borrowed `mog.name` occurs here
14 | }
   | - borrow might be used here, when `another` is dropped and runs the `Drop` code for type `ContainsReference`

For more information about this error, try `rustc --explain E0506`.

There’s dedicated logic in the compiler that checks the interaction of destructors and lifetimes.

5 Likes

Great point, I appreciate that clarification!

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.