I've been trying to find a good explanation of Rust lifetimes. I get the idea that they are our way of telling the compiler which variables have to outlive which, so it can verify that references are non-dangling without having to check every code path. What I don't get is what we are actually saying when we use them. Take this example:
fn foo<'a>(bar: &'a i32, baz: &'a i32) -> &'a i32 {
bar
}
fn main() {
let m = 5;
let x = &m;
{
let n = 6;
let y = &n;
{
let z = foo(x, y);
dbg!(z);
}
dbg!(y);
}
dbg!(x);
}
This compiles and runs fine. Here x, y, and z all live for different lengths of time. The underlying data, m and n, survive for different times as well. So clearly when we declared the arguments and the return value of foo to all have lifetime 'a, we weren't saying they all live for the exact same stretch of the program. (I'm trying not to say "lifetime", sorry for the awkward phrasing.)
The other interpretation that I've heard is we're saying there exists some lifetime 'a at the intersection of the lifetimes of bar, baz, and the return value. However, this would be a trivial statement, as clearly they are all in scope at the moment we call foo.
What it seems to me we are saying, is that there exists some lifetime 'a such that bar and baz outlive 'a, and the return value is outlived by 'a. This would mean that lifetime annotations have a different meaning when used on arguments versus when used on return values: it would mean that annotations on arguments are lower bounds on the lifetime and annotations on return values are upper bounds. This interpretation makes the most sense, but I have a feeling that this isn't correct either, because I think &'a T is (sort of like) a type, and it wouldn't make sense for a type to have a different meaning on arguments versus on return values. If the same notation has a different meaning in different places, then wouldn't it add a layer of impurity into the language where the same syntax can have different meanings and we just have to memorize which places it has each meaning?
So can anyone explain what lifetimes actually mean?
As a part two to this question, can we also explain what lifetimes mean in a struct definition? I'm asking for this as well because I suspect the answer is different than for function definitions. Here's an example:
struct Foo<'a> {
bar: &'a i32
};
fn main() {
let foo = Foo { bar: &5 };
}
Again, here it seems like we're saying that foo.bar outlives 'a and foo is outlived by 'a. So again we have this different meaning for lifetimes on the struct template parameter versus on the struct member.