The short version is that when you make a call like choose_first(cs, cf)
, and cs
and cf
are references, it's sort of like you just freshly borrowed those references again and they can have shorter lifetimes than before -- so short they end on the same line as the println
for example.
Let's look a little closer:
// `<'a: 'b, 'b>` reads as lifetime `'a` is at least as long as `'b`.
// Here, we take in an `&'a i32` and return a `&'b i32` as a result of coercion.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
first
}
fn main() {
let first = 2; // Let's say first: i32 + 'f
let cf = &first; // And cf: &'cf i32
{
let second = 3; // And s: i32 + 's
let cs = &second; // cs: &'cs i32
// See Below
println!("{} is the first", choose_first(cs, cf));
// 'cs ends here
// 's ends here
// 'cf and 'f could have ended here too since you didn't use them any more...
};
// But you can force them to live all the way to here by using them
println!("{}", cf);
// 'cf ends here
// 'f ends here
}
So in this version, 'cf
definitely lasts longer than 'cs
as I have written them.
And the signature of choose_first
says the first reference must last as long as (or longer than) the second reference.
So why did the call to choose_first
work? It works because a &
reference with a shorter lifetime is considered a sub-type of a &
reference with a longer lifetime. It works because a &
reference with a longer lifetime is considered a sub-type of a &
reference with a shorter lifetime. So when you call choose_first
, the shortest pair of lifetimes possible are chosen. It's sort of like this happened:
{
let temp_cf = &*cf; // Some new shorter borrow
let temp_cs = &*cs; // Same
println!("{} is the first", choose_first(cf, cs));
// The borrows end here
}
This system is usually presented formally by explaining variance, which unfortunately, can be complicated and confusing. A shorter version is "the compiler can shrink the lifetimes of your &
references at the call site." That's what "covariant" means in the link.
Bonus example: If you look on that variance page, it says:
b: &'b mut B, // covariant over 'b and invariant over B
When a type is invariant, the lifetime can't be shrunk (or grown). So this says that if we used something with a lifetime that was itself behind a &mut
, the inner lifetime can't be shrunk. Let's see:
// I only care about enforcing the lifetime bounds here
fn zero<'a: 'b, 'b>(_: &'a str, _: &mut &'b i32) -> u32 {
0
}
fn main() {
let first = 2;
let mut cf = &first;
let ccf = &mut cf;
{
let second = "foo".to_string();
let cs = &second;
println!("{} is the zero", zero(cs, ccf));
};
println!("{}", cf);
}
And yep, this one fails to compile. The compiler couldn't shrink the borrow because it was behind a &mut
. (The phrasing of the error is a bit odd.)
If you remove the last println
, the lifetime can be short enough from the beginning and that also works. (This is what I meant before when I said the compiler tries really hard to find some lifetimes that allow the code to compile.)