Generic associated types lifetime

Sometimes I don't understand the lifetime constraints with GATs. Here is an example. The following code compiles just well:

#![feature(generic_associated_types)]
pub trait Foo {
  type T<'a>;
}

impl<'t, T> Foo for &'t T {
  type T<'a> = Self;
}

As far as I understand, having type T<'a> = Self accepted by the compiler implicitly mean that Rust puts the implicit bound Self: 'a, since &'t T is not 'static'. Otherwise the type lifetime parameters ('a) may outlive the type definition (Self/&'t T), right? So my assumption is that when we have type A<'a> = B then Rust implicitly add the bound B: 'a.

However the following does not compile:

impl<'t, T> Foo for &'t T {
  type T<'a> = &'a Self;
}

I get

error[E0491]: in type &'a &'t T, reference has a longer lifetime than the data it references

So this time, the bound &'a Self: 'a does not hold, even though previously we had Self: 'a? Which also mean my assumption is wrong.

This problem can be solved by explicitly adding a where Self: 'a bound in the trait definition and impl, but obviously there is something wrong with my reasoning. Can anyone explain what?

There is no implicit bound. Trait::Gat<'a> can outlive 'a and Self. But it does need to be well-formed.

Self / &'t T are implicitly well-formed as it is an input to the impl. (Search forward for "Trait impls".) But &'a &'t T is not, unless Self (and thus 't in this case) outlives 'a.

See also this request for feedback, wherein scenarios for implicitly adding the Self: 'a bound (or not) are currently being considered. There are more examples of GATs not tied to Self in the comments.

2 Likes

Thanks, that was helpful. If I understand well, the GAT type A<'a> = B is accepted as long as B is WF, without actually checking that B always outlives the 'a lifetime. That explains why we can replace B with Self (or &'t T), even though there is no proof of 't: 'a. That doesn't sound right to me, I feel like it should be rejected without that (just as &'a &'t T is), but I guess as long as 't: 'a is valid when A<'a> is instantiated then it is not required for its definition.

It's also not required that B: 'a when instantiated. This deserves a more in-depth dive than I can give it right now, but here's a shallow example. The error when you uncomment either of the let r = lines hints at the deeper waters:

   = note: the type is valid for the static lifetime
note: but the borrow lasts for the lifetime `'s` as defined on the function body at 17:8

We apparently have some "type validity lifetime" at play, distinct from, I don't know, "instantiated type lifetime". Early bound and late bound, perhaps. However, the type validity lifetime doesn't apply any restriction to the instantiated type lifetimes as far as I'm aware:

  • In this example we have a <_ as _>::T<'static> producing something with a limited lifetime
  • In my previous example, a <_ as _>::T<'s> produced a &'static str even when 's was not 'static

What's going on? The short answer is that GATs are polymorphic, and thus less infectious / more "pure"ly type constructors / less constrained than non-associated generic types. Namely a GAT is not constrained by it's lifetime parameters the way a non-associated generic type is by RFC 1214. They just guide the way to the normalized type.

Does the "type validity lifetime" play any role or is it just a historical artifact of pre-GAT code shining through? I'm not actually sure, and sadly don't have the time to investigate more right now.

And again, I agree this all deserves a more thorough write-up (alongside some related topics which would benefit from being consolidated).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.