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
?
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.