Why isn't lifetime updated when mutating references?


#1

Why doesn’t the following program compile?

struct Foo;
fn main() {
    let a = Foo;
    let mut x: &Foo;
    let b = Foo;
    x = &b;
    x = &a;
}

The error message complains that b doesn’t live long enough, but clearly b is valid during the entire time when x refers to it. It seems that the borrow checker fails to update the lifetime of a reference when the reference refers to a new value.


#2

I’m a beginner, but I think the problem is, that the variables gets dropped in reverse order.

x = &b;
// b gets dropped here, but it is borrowed by x
x = &a;

This fixes the problem:

struct Foo;
fn main() {
let a = Foo;
let b = Foo;
let mut x: &Foo;

x = &b;
x = &a;

}

Bu I am really not sure, if my explanation is correct.


#3

Thanks, but I disagree, because b 's lifetime should last until main() returns, and certainly after x=&a.


#4

Well yes and no. How are they supposed to get dropped? The answer is in reverse order. So, if you have following code, the drop order is following:

fn main() {
let a = Foo
let b = Foo
drop b
drop a
}

This is a fix order what you can only change by changing the order of the declaration.
And if a has borrowed b, then b can’t be dropped. So b has to be declared before a.

But I have to admit: Your example makes it difficult to me to justify this implementation. I think the compiler should know, that x gets mutated with

x = &a;

and should wait with dropping b.


#5

But as I understand it, a value is dropped when it goes out of scope. In my original example, b won’t go out of scope until the end of main(), so it is valid during the entire time while x refers to it.


#6

I think, your code shouldn’t be a problem, if the drop order would be following:

struct Foo;
fn main() {
let a = Foo;
let mut x: &Foo;
let b = Foo;

x = &b;
x = &a;

drop b;
drop x;
drop a;
}

But somehow it gets dropped here

x = &b;
drop b;

I don’t get it, why the compiler is so strict.


#7

It is too complicated for real usage.

x may instead be a structure that borrows Foo and implements Drop.
Program will likely have steps between the change of reference.
The program does not know there will not be a panic.
A panic would then unwind; drop b before dropping x. But x still may want to access b inside of drop().

Even without panic, loops and conditionals would make determining the lifetime for each time the reference changes too complicated.


#8

Short answer: because lifetime is a property of a type, and the type of a variable can’t change. So the refence’s lifetime must be broad enough to cover the life of both variables. There’s no such thing as “updating the lifetime”.

Now why b doesn’t live long enough? This drop order is correct:

Currently in Rust it’s not allowed for a reference to even live longer than the referred variable. This rule may sound weird, but it exists to treat all types equally – if x was a struct containing a reference to b, which accessed b's contents on drop, this code would be clearly wrong. Now, there’s a proposal called non-lexical lifetimes, which would make your code compile. This proposal was appearing since I remember, because it will be major usability improvement, but was for compiler refactor (MIR) and stability.


#9

The compiler needs to have a defined order in which each value is dropped, because if they implement Drop, code will run at that point. You can think of every let as introducing an invisible scope, as in this (not valid Rust, but similar to some other languages like OCaml):

fn main() {
    let a = Foo in {
        let mut x: &Foo in {
            let b = Foo in {
                x = &b;
                x = &a;
            }
        }
    }
}

Usually, this doesn’t matter, but sometimes you have to reorder your lets. Its possible improvements to the borrowchecker will solve this in the future.