Does this mean a trait live longer than 'a?

In the reference, there's this following example code:

// For the following trait...
trait Bar<'a>: 'a { }

I didn't understand this usage, so I googled and found the most related one. It basically means the implementer needs an external "object"'s lifetime, if I took it correctly. But this blog doesn't contain anything related with the : 'a part. Does this mean the implementer must also outlive 'a? And what's the usage of this syntax? And if this means the implementer needs to outlive 'a, and trait Bar<'a> means 'a must outlive the implementer, then this may be contradictory? I'm just totally confused by this.

Yes, 'a is a bound on the implementer.

It is syntactic sugar for

trait Bar<'a> where Self: 'a {}

which makes the fact that the implementer must outlive 'a more clear.

2 Likes

Thank you for the link to the reference's definition for this syntax, which makes it very clear. But does the prior part trait Bar<'a> mean the implementer should depend on 'a? I just want to make sure my understanding is correct. And then trait Bar<'a>: 'a will basically be useless because the implementer has the exact lifetime as 'a.

I'm not exactly sure what you mean by the implementer depending on 'a. Also I don't see why trait Bar<'a>: 'a just from its signature should be useless. I could imagine some sort of deserialisation trait that looks like this

trait MyDeserialise<'de>: 'de {
    type Error: std::error::Error;

    fn from_str(s: &'de str) -> Result<Self, Self::Error>;
}

to warrant the Self: 'de bound, for example.

I now kind of understand the usage of it. Thank you!

1 Like

I'm going to try to demonstrate some practical implications of having lifetime bound on a trait. It will touch on some moderately advanced topics. TL;DR: there's a good chance you don't need or want the bound.


First a couple of technical notes.

  • An outlives relation on a type, T: 't, is defined syntactically, and requires that any lifetimes 'x found within T meet a 'x: 't bound.

  • In order to be able to type erase M to dyn Trait + 'm, the bound M: 'm must hold. You can think of the + 'lifetime on a dyn Trait as being a bound you know the erased base type meets.

Here's a (non-practical) example where the bound on the trait matters. If you delete the + 'de bound on the trait, the example function will fail. The reason is that the generic M might not meet the 'de bound anymore -- it might contain a lifetime that doesn't meet the bound, since the trait didn't require that in the implementation. Note that this is true whether M contains 'de or not. (Like the help says, you could put the bound on the function instead if you wanted.)

Here's @jofas' example, slightly modified. The example is now a bit more practical (but still somewhat contrived). If you delete the + 'de bound on the trait, this example function will also fail.

The reason why the 'de bound is load bearing in this example is related to the syntactical nature of outlive bounds. If M: 'de, it doesn't contain any lifetime 'short which doesn't meet the bound 'de. That means it's impossible for the trait implementation of MyDeserialise<'de> for M to name a too-short lifetime when defining the associated type Error.[1] The compiler can therefore conclude that <M as Deserialise<'de>>::Error: 'de.

This RFC has more details about outlives relations and "projections" (outlives relations of associated types in a generic context), though it's a bit dense.


Now let's switch gears a bit and see some implications of the bound from the implementor's perspective. Here, we've implemented the trait with the bound for a type that also has an independent lifetime 's. If you remove where 's: 'de, the implementation won't compile anymore. This demonstrates what I was saying about 'short above. The 's: 'de bound is required even though in the implementing type, 's didn't have anything to do with 'de. This often doesn't cause too many problems anyway,[2] when the lifetimes are covariant (as they are in the example). Perhaps because if there's code that cared about how long the implementing type lived anyway, it would have had to deal with the case where 's: 'de already.

But let's look at a case where the implementor has an independent lifetime, and doesn't have 'de. As before, the where 's: 'de is required on the implementation due to the bound on the trait. But now the implementing type is completely unrelated to 'de. The implementation could work for any 'de, just like it would for a T: 'static implementor. But the 's: 'de bound is still required because it is on the trait. This sometimes can cause problems.

For example, if you uncomment the hrtb call, you'll see that it fails. But if you remove the bound on the trait and the 's: 'de bound from the implementation, it succeeds.

Related problems come up around GATs fairly often. The workarounds are more involved than I want this comment to be, but here's an article about that. The workarounds are required when the trait depends on the Self: 'de bound, but you still want higher-ranked trait bounds to be functional. They emulate a conditional higher-ranked bound: for<'de where M: 'de> ....

The workaround notably avoids putting the bound on the trait, as that defeats the higher-ranked goal in the same way as the example above.


Now that I've demonstrated a potential pitfall or limitation of putting the lifetime bound on the trait, let's take a step back and ask: what was gained by adding the bound? The trait definition didn't need it, and our examples where we took advantage of it were somewhat contrived and could be worked around locally. In my experience, if you have a lifetime on a trait and a generic type that implements it for one specific lifetime, you usually still don't care about the lifetime of the implementor -- and if you do, you can use a distinct lifetime for that bound.

But when we cared about HRTBs -- about meeeting a bound for all lifetimes -- the bound on the trait got in the way.

I think a lot of lifetime bounds get added to traits out of instinct or misunderstanding, and not an actual need.[3] They also have some odd niche effects which I didn't go into. The main exception is a 'static bound, which is required for things like the Any trait.

Therefore, if you don't have an explicit reason to add such a bound, my advice is to not add it. (And if you think you have an explicit reason, test it, as it may not do what you think.)


  1. The trait parameters have to be considered too, but in this case they are just 'de, and 'de: 'de. ↩︎

  2. in my experience ↩︎

  3. At least I've seen a lot of bounds that don't have any clear benefit to the trait itself. ↩︎

3 Likes

That's a thorough and advanced post. I will need to peruse and digest it for some days. Thanks very much, because this can really improve my understanding of lifetime.