rustlifetimeborrow

How does Rust calculate lifetime of a moved value?


In Rust, the lifetime of a value is between the point of definition and the point of going out of scope.

However the out of scope can be the end of a statement that steals the value.

Rust respects this only partially. Why?

Take this example:

// This is the starting point, it compiles without errors/warnings, but does not do what it should.
struct Inner{x : i32}

struct Outer<'e>{
    s : &'e Inner,
    c : i32
}

fn print_inner_value(mut o : Outer) {
    println!("o.s.x = {:?}", o.s.x);
    o.c += 1;
}

fn change_inner(mut o : Outer) {
    let new_i = Inner{x : 40};
    o.c += 2;
    //o.s = &new_i;
    print_inner_value(o);
    println!("new_i.x = {:?}", new_i.x);
    //o.c += 3;
}

fn main () {
    let orinal_i = Inner {x : 10};
    let mut o = Outer{s : &orinal_i, c : 0};
    o.c += 4;
    change_inner(o);
}

What I really want is for the line o.s = &new_i; to not be commented.

But if I do that, I get E0597 saying that new_i does not live long enough.

But it appears to live long enough, because if I instead uncomment o.c += 3; then I get E0382 saying that o.c cannot be used since it has been moved.

Clearly at println!("new_i.x = {:?}", new_i.x); line, value new_i is alive and value o was moved into a function that has ended so it should no longer be alive.

So the question is: Why moving a value shrinks the scope of it, but does not shrink the lifetime of it?


Solution

  • Your mistake is assuming that the lifetime of 'e represents the lifetime of Outer: it doesn't.

    Instead, since a value o of type Outer<'e> contains a reference with a lifetime of 'e, then the lifetime of this value o must be shorter (or equal) to 'e -- lest the value o outlives the referred to element, and ends up with a dangling reference.


    The real issue you are hitting is that the lifetime parameters of a function ('e, here) are determined by the caller, and fixed within the function itself.

    When you create the instance o in main, a lifetime 'e is calculated matching the lifetime of orinal_i.

    Then, within change_inner, when you set about changing o.s, the type system requires that whatever reference you assign must have a lifetime greater than or equal to this predetermined 'e, that is greater than or equal to the lifetime of orinal_i.

    That the value of o, containing said reference, will soon disappear has no impact on the required lifetime.

    This may be observed better by looking at the non-elided version of the function:

    fn change_inner<'e>(mut o: Outer<'e>) {
        let new_i = Inner{x : 40};
        o.c += 2;
        //o.s = &new_i;
        print_inner_value(o);
        println!("new_i.x = {:?}", new_i.x);
        //o.c += 3;
    }
    

    The 'e above is the lifetime of a reference outside the function, thus a lifetime which outlives the function. new_i, a variable created within the function, has a shorter lifetime than 'e, and therefore o.s cannot be bound to &new_i.

    Another way to see it is that scope of o has an impact on how long orinal_i is borrowed for, but does not impact the lifetime of orinal_i: whether o lives a short or long life, it does not change how long the life of orinal_i is, just how long o accesses orinal_i.


    In your particular case, a work-around exists: creating a new Outer value.

    A new value can have a new type, one in which the 'e parameter is NOT fixed, and the compiler will be able to infer a new "value" for the lifetime, a value shorter than that of &new_i:

    fn change_inner<'e>(o: Outer<'e>) {
        let mut neo = o;
    
        let new_i = Inner{x : 40};
        neo.c += 2;
        neo.s = &new_i;
    
        print_inner_value(neo);
    
        println!("new_i.x = {:?}", new_i.x);
    }
    

    Here, the compiler selects a lifetime for neo's 'e which works for both o.s and &new_i -- the shorter of the two, really.

    Do note there's quite a bit of magic at play: