12

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
user1338952
  • 3,233
  • 2
  • 27
  • 41

1 Answers1

8

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.

carols10cents
  • 6,943
  • 7
  • 39
  • 56
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Maybe *foo<...>(...b: &str)* should be *foo<...>(...b: 'b &str)*? Strange no compiler error or warning on unused scope 'b. With original question I thought need for different lifetimes was very contrived. But hey if it can happen just make them independent. I think this example showing advantage of same lifetimes is also very contrived. Who knows upfront (or should know) all potential lifetime issues client's may face? Letting extra typing be guide I'm back to just setting them same. Unfortunately, once choice is made changing is not easy. – user1338952 Jun 23 '17 at 13:24
  • @user1338952 yes, the vast majority of examples are contrived; that's how examples work. Your second point is called *software design*. For example, why would you ever write `add(a: u8, b: u8)` when you could write `add(a: A, b: A)` or when you could write `add(a: A, b: B)` or when you could write `add(a: A, b: B)`? Thinking about how your code will be used is the thing that makes software both challenging and rewarding. Yes, we sometimes get it wrong, sometimes we have to make (breaking) changes. – Shepmaster Jun 23 '17 at 13:41
  • Any blogs that cover topic (i.e. *software design* choices when choosing lifetimes for your structs)? This is a bit more than: this is how it works. Pre-release book *Programming Rust* has section *Distinct Lifetimes Parameters* which touches on issue. Seems suggestion is try same first, if you find you need them independent change them. Not sure that scales. – user1338952 Jun 23 '17 at 14:51
  • @user1338952 I don't know that there *is* a right way; that's what I was getting at with my generic types example. You could start from a very generic point of view and make the API more complicated for potentially no benefit, or you could start from a simpler point of view and add complexity as it emerges. I prefer to start from the simpler and grow more complex, but I know there's another frequent SO contributor that starts with distinct lifetimes and only unifies them later. I know of no longer blog posts on the topic, unfortunately. – Shepmaster Jun 23 '17 at 14:57