This question is similar to When is it useful to define multiple lifetimes in a struct?, but hopefully different enough. The answer to that question is helpful but focuses on advantages of one approach (using distinct lifetimes for references in struct) but not on drawbacks (if any). This question, like that, is looking for guidance on how to choose lifetimes when creating structs.
Call this the tied together version because x and y are required to have the same lifetime:
struct Foo<'a> {
x: &'a i32,
y: &'a i32,
}
and call this the loose version because lifetimes can vary:
struct Foo<'a, 'b> {
x: &'a i32,
y: &'b i32,
}
The answer to the referenced question gives a clear case where client code can compile/run given the loose version but will fail for the tied together version. Isn't it the case that any client code that works for the tied together version will also work for the loose version and will be guaranteed just as safe (i.e. safe)? The obverse is not true. The loose version is clearly more flexible from a struct designer perspective. Given it is a good/accepted answer the guidance might be - when using references in a struct always give them distinct lifetimes.
What is the drawback to this advice, ignoring the extra typing? For example, is there ever benefit to requiring references in a struct have the same lifetime?
is there ever benefit to requiring references in a struct have the same lifetime
Yes, and it goes beyond having a struct. If lifetimes were always distinct from each other, then you couldn't write this function:
fn foo<'a, 'b>(a: &'a str, b: &'b str) -> &str {
// What lifetime to return?
if (global_random_number() == 42) {
a
} else {
b
}
}
Applying to the struct, you could have something like this:
struct EvenOrOdd<'a, 'b> {
even: &'a str,
odd: &'b str,
}
impl<'a, 'b> EvenOrOdd<'a, 'b> {
fn do_it(&self, i: u8) -> &str {
if i % 2 == 0 {
self.even
} else {
self.odd
}
}
}
Note that while this compiles, it doesn't return a string that can outlive the structure itself, which is not what was intended. This code fails, even though it should be able to work:
fn foo<'a, 'b>(a: &'a str, b: &'b str) {
let result = { EvenOrOdd { even: a, odd: b }.do_it(42) };
println!("{}", result);
}
This will work with unified lifetimes:
struct EvenOrOdd<'a> {
even: &'a str,
odd: &'a str,
}
impl<'a> EvenOrOdd<'a> {
fn do_it(&self, i: u8) -> &'a str {
if i % 2 == 0 {
self.even
} else {
self.odd
}
}
}
This is the opposite of the linked answer, which has the comment:
you want to be able to take an aggregate value and split off parts of it after using it
In this case, we want to take an aggregate value and unify them.
In rarer occasions, you may need to thread the needle between distinct and unified lifetimes :
struct EvenOrOdd<'a, 'b: 'a> {
even: &'a str,
odd: &'b str,
}
impl<'a, 'b> EvenOrOdd<'a, 'b> {
fn do_it(&self, i: u8) -> &'a str {
if i % 2 == 0 {
self.even
} else {
self.odd
}
}
}
While this is useful when needed, I can't imagine the wailing and gnashing of teeth that would erupt if we had to write it this way every time.
ignoring the extra typing
I wouldn't. Having
foo<'a>(Bar<'a>)
is definitely better than
foo<'a, 'b', 'c, 'd>(Bar<'a, 'b', 'c, 'd>)
When you aren't benefiting from the extra generic parameters.