When is it useful to define multiple lifetimes in a struct?
RustLifetimeRust Problem Overview
In Rust, when we want a struct to contain references, we typically define their lifetimes as such:
struct Foo<'a> {
x: &'a i32,
y: &'a i32,
}
But it's also possible to define multiple lifetimes for different references in the same struct:
struct Foo<'a, 'b> {
x: &'a i32,
y: &'b i32,
}
When is it ever useful to do this? Can someone provide some example code that doesn't compile when both lifetimes are 'a
but does compile when the lifetimes are 'a
and 'b
(or vice versa)?
Rust Solutions
Solution 1 - Rust
After staying up way too late, I was able to come up with an example case where the lifetimes matter. Here is the code:
static ZERO: i32 = 0;
struct Foo<'a, 'b> {
x: &'a i32,
y: &'b i32,
}
fn get_x_or_zero_ref<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 {
if *x > *y {
return x
} else {
return &ZERO
}
}
fn main() {
let x = 1;
let v;
{
let y = 2;
let f = Foo { x: &x, y: &y };
v = get_x_or_zero_ref(&f.x, &f.y);
}
println!("{}", *v);
}
If you were to change the definition of Foo
to this:
struct Foo<'a> {
x: &'a i32,
y: &'a i32,
}
Then the code won't compile.
Basically, if you want to use the fields of the struct on any function that requires it's parameters to have different lifetimes, then the fields of the struct must have different lifetimes as well.
Solution 2 - Rust
Here is another simple example where the struct definition has to use two lifetimes in order to operate as expected. It does not split the aggregate into fields of different lifetimes, but nests the struct with another struct.
struct X<'a>(&'a i32);
struct Y<'a, 'b>(&'a X<'b>);
fn main() {
let z = 100;
//taking the inner field out of a temporary
let z1 = ((Y(&X(&z))).0).0;
assert!(*z1 == z);
}
The struct Y
has two lifetime parameters, one for its contained field &X
, and one for X
's contained field &z
.
In the operation ((Y(&X(&z))).0).0
, X(&z)
is created as a temporary and is borrowed. Its lifetime is only in the scope of this operation, expiring at the statement end. But since X(&z)
's lifetime is different from the its contained field &z
, the operation is fine to return &z
, whose value can be accessed later in the function.
If using single lifetime for Y
struct. This operation won't work, because the lifetime of &z
is the same as its containing struct X(&z)
, expiring at the statement end; therefore the returned &z
is no longer valid to be accessed afterwards.
See code in the playground.
Solution 3 - Rust
I want to re-answer my question here since it's still showing up high in search results and I feel I can explain better. Consider this code:
struct Foo<'a> {
x: &'a i32,
y: &'a i32,
}
fn main() {
let x = 1;
let v;
{
let y = 2;
let f = Foo { x: &x, y: &y };
v = f.x;
}
println!("{}", *v);
}
And the error:
error[E0597]: `y` does not live long enough
--> src/main.rs:11:33
|
11 | let f = Foo { x: &x, y: &y };
| ^^ borrowed value does not live long enough
12 | v = f.x;
13 | }
| - `y` dropped here while still borrowed
14 | println!("{}", *v);
| -- borrow later used here
What's going on here?
- The lifetime of
f.x
has the requirement of being at least large enough to encompass the scope ofx
up until theprintln!
statement (since it's initialized with&x
and then assigned tov
). - The definition of
Foo
specifies that bothf.x
andf.y
use the same generic lifetime'a
, so the lifetime off.y
must be at least as large asf.x
. - But, that can't work, because we assign
&y
tof.y
, andy
goes out of scope before theprintln!
. Error!
The solution here is to allow Foo
to use separate lifetimes for f.x
and f.y
, which we do using multiple generic lifetime parameters:
struct Foo<'a, 'b> {
x: &'a i32,
y: &'b i32,
}
Now the lifetimes of f.x
and f.y
aren't tied together. The compiler will still use a lifetime that's valid until the println!
statement for f.x
. But there's no longer a requirement that f.y
uses the same lifetime, so the compiler is free to choose a smaller lifetime for f.y
, such as one that is valid only for the scope of y
.