Search code examples
rustlifetime

Rust Lifetimes Behaviour


I am trying to understand lifetimes through the Programming Rust book, and made a custom example here:

fn simple_fn(point: [&i32; 3]) -> (&i32, &i32) {
    (point[0], point[2])
}

fn main() {
    let mut point: [&i32; 3] = [&19, &18, &17];

    {
        let x = 3;
        point[1] = &x;
        point[1] = &10;
    }

    let t = simple_fn(point);
    println!("{:?}", t);
}

The compiler returns an error:

error[E0597]: `x` does not live long enough
  --> src/main.rs:10:20
   |
9  |         let x = 3;
   |             - binding `x` declared here
10 |         point[1] = &x;
   |                    ^^ borrowed value does not live long enough
11 |         point[1] = &10;
12 |     }
   |     - `x` dropped here while still borrowed
13 |
14 |     let t = simple_fn(point);
   |                       ----- borrow later used here

I don't understand, when I have re-assigned point[1] to &10, right after its assignment to x, then why is lifetime of point[1] still limited to lifetime of x?


Solution

  • I assume the confusion comes from the fact that the following is valid:

    let mut point = &1;
    
    {
        let x = 3;
        point = &x;
        point = &10;
    }
    
    println!("{:?}", point); // Prints `10`
    

    The above is valid, because the compiler can infer that i is completely replaced. So i from that point forward, has the lifetime of &10 instead of &x.


    Let's use an easier example to understand why your example doesn't work.

    Let's consider a Vec<&i32> instead. Everything remains the same, but instead of indexing, let's use .push() and .clear().

    If we instead .push(&x) does the example then work? What if we .clear() before .push(10), does it work then?

    let mut point = Vec::new();
    
    {
        let x = 3;
        point.push(&x);
    
        point.clear();
    
        point.push(&10);
    }
    
    println!("{:?}", point);
    

    Does the above compile?

    No, it results in the exact same error as the one you're encountering. Why? Well, that might be a lot more clear now. Yes, we're calling .clear() which results in &x being removed. However, the compiler doesn't know that, by simply looking at the function definition of clear(). So the lifetime tied to the Vec<&i32> remains as the shortest (observed) lifetime by the compiler, which is that of &x.


    Now, when you do:

    points[1] = &x;
    

    That is actually just syntactic sugar for:

    *point.index_mut(1) = &x;
    

    So again, looking at fn index_mut() the compiler doesn't know from that, that some index would extend the lifetime of the container.

    Again, the reason the compiler knows in the first example. Is because point is reassigned, so it receives a new lifetime from that point forward. However, calling an fn(&mut self, usize) method doesn't tell anything about that.


    In theory, there could be a special case in the compiler, where when a constant index is used, then it would extend the lifetime of the container in this scenario.

    However, such a special case might quickly end up being confusing. Since now if you replace a constant 1 with an index variable. Then the lifetime of the container could drastically change out of nowhere.

    // With a special case, this would then pass
    point[1] = &x;
    
    // Whereas this would fail
    let i = 1;
    point[i] = &x;
    

    Additionally, it's way more likely that someone is indexing a container using an index variable. So the special case would also rarely kick in.