Hacker News new | past | comments | ask | show | jobs | submit login

More accurately, it allows the lifetimes to _be_ different.

Think of it like generics. What's the difference between:

    struct Foo<T> {x: T, y: T}
and

    struct Bar<T,U> {x: T, y: U}
"How's T different from U"? That's not the right question to ask -- over here Bar lets T be different from U, whereas in Foo both x and y must have the same type.

With the example above

    struct Example<'a, 'b>{ ref1: &'a i32, ref2: &'b i32}
this means that there are two references of independent lifetimes. It could also be:

    struct Example<'a>{ ref1: &'a i32, ref2: &'a i32}
which means that the lifetimes must match, or even

    struct Example<'a, 'b: 'a>{ ref1: &'a i32, ref2: &'b i32}
which means that 'b is a subset of 'a. (This becomes useful if a struct stores a borrow to a thing and then a borrow of something extracted from that thing)

While the first case is the most common (and theoretically could be elided), it's still often a tossup and it's clearer when you know that a struct is borrowing things.




Aren't there subtype coercions that make this syntax useful only rarely? (something like: if 'b: 'a, then &'a and &'b will both be treated as 'b for this instance, or maybe the other way around? And with only lexical lifetimes, for two references visible in the same scope, one is necessarily a subtype of the other, no?)

The only time I've ever used multiple explicit lifetimes, I was accidentally trying to trick the compiler into accepting shared mutability. The compiler was not fooled.


Yes, you're correct. You need this syntax when the borrow checker forces you to add this dependency, i.e. when you're stuffing one reference inside another, or swapping references, or whatever.

This is why elision is so awesome, it takes care of almost all the cases where you need a "regular" lifetime specification on a function. When elision doesn't works usually there is something you need to specify.

The subtyping works for callers; lifetimes get stretched and sqeezed to fit the function signature. But the function body itself won't compile when you're doing things like stuffing references into other references unless the lifetimes are right.


Thank you. The article made it sound to me like struct Foo<'c>, 'c is the lifetime of the struct. Am I correct in thinking that, in reality, 'c must live as long or outlive the struct?


Yeah, the lifetimes on a struct are the lifetimes of things that the struct points to, not an instance of the struct itself. For an instance to be valid, everything it points to must be valid, so if `foo: Foo<'c>`, `foo` cannot be stored for longer than `'c`. E.g. this code is invalid, because the `Foo` instance would live longer than the thing it points to:

  let foo = {
      let age = 0;
      Foo { age: &age }
  };
The `age` variable goes out of scope at the } (i.e. the maximum possible lifetime for &age ends there), and hence letting the `Foo` escape from that scope would be a bad idea.

(NB. this applies to the "core" lifetime'd types like `&'c T` too: that is really just a nicer way to write a parameterised type like `Ref<'c, T>`. So the rule "cannot return a reference to a local variable" is the same rule that stops a `Foo<'c>` living too long.)


Makes perfect sense. Thank you!


The article is wrong/misguided. See my comment above :)

When you say `Foo<'c>`, all you're saying is that each `Foo` instance has an associated lifetime `'c`, similar to a type parameter. When you add the `x: &'c u8` field, then you're saying that "this lifetime is the lifetime of the inner u8".

> Am I correct in thinking that, in reality, 'c must live as long or outlive the struct

For the specific example, yes. It's possible to have an example where 'c must be shorter lived, and cannot outlive the struct.

If you had an &mut instead of & there, then 'c must live exactly as long as the struct.


Thank you.

> It's possible to have an example where 'c must be shorter lived, and cannot outlive the struct.

I'm interested in such an example. Could you provide one?


Actually, I just looked into it, that's no longer possible. I was talking about contravariant lifetime positions, which happen with functions, but now it seems like function pointers are invariant in those positions, not contravariant.

The idea was to have something like

    struct Foo<'a> {f: fn(&'a u8)}
should only be able to accept pointers that live shorter than the struct. But contravariance seems to have been removed.


Makes sense. I can see how having contravariant function arguments could produce some surprising behavior.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: