Issues with explicitly defining lifetimes


#1

Hi guys,

I’m having issues explicitly defining my lifetime parameters. The un-annotated version compiles, found here: https://play.rust-lang.org/?gist=6e579bd7ce36e455a9a68cee56f27f4f&version=stable

My attempt at solving doesn’t compile, found here: https://play.rust-lang.org/?gist=df0f8d0d4dd92124a910babc0e676367&version=stable

Follow up attempt here: [playground]?gist=b3ef9cd768239f4ac40bbb5e76156eed&version=stable
!! I’m currently not allowed to post more than 2 links !!
But the second attempt fails with a different error, which i don’t understand. I figured impl<'a,'b> is essentially the same as def xx<'a,'b>?

Context: I’m building playground examples around compiler errors related to borrowing and lifetimes. My intention is to setup examples which explicitly fail. Afterwards updating the code and annotating why the failures occurred.
This specific playground example has kept me already busy for a few days. It feels like i still have a lot to understand…

Edited: cleanup newlines and spaces


#2

In general, you never want to put any explicit lifetime annotation on &self. I have never seen it necessary, and it only can make things overcomplicated.

Keep in mind that lifetimes don’t make code do anything (they have absolutely zero influence on generated code). They only describe what the code does and is allowed to do.

So when you create a trait like: fn get_ref<'a>(&'a self) -> &'a u32; then you’ve explicitly forbidden yourself from using different lifetimes for self and u32 (it says that the u32 can never ever be used after the self is gone).

So the implementation later as fn get_ref(&self) -> &'ax u32 is not allowed, because it does say that u32 can outlive self, which the trait was designed to restrict.

You can implement it like this: (it’s equivalent to not specifying the lifetimes at all)

fn get_ref<'a>(&'a self) -> &'a u32 {
        &self.the_ref
}

and in the &self.the_ref line compiler will “cast” the longer 'ax lifetime to 'a implicitly.

If you do want to allow get_ref to return something that lives longer than self, then this is the way to do it: (annotations on self here are not needed, I’ve just added them for contrast)

trait InnerTrait<'long> {
    fn get_ref<'short>(&'short self) -> &'long u32;
}

struct InnerStruct<'a> {
    // Note: the_ref must outlive 'a (inherit from 'a).
    the_ref: &'a u32,
}

impl<'contents> InnerTrait<'contents> for InnerStruct<'contents> {
    fn get_ref<'container>(&'container self) -> &'contents u32 {
        self.the_ref
    }
}

#3

Hi @kornel, thanks for your reply.

I updated the example with your suggestion, see below.

That makes perfect sense, i wasn’t yet on that level of comprehension. My original idea was that the lifetime argument indicate relations between lifetimes, not that they have to be interpreted as actual replaceable tokens.

https://play.rust-lang.org/?gist=f3b6c003900edab63312fa46ed110c6e&version=stable
This still doesn’t compile; The compiler has problems inferring a proper lifetime for 'contents, while keeping the constraint that 'contents is not allowed to outlive 'container.

I guess line 23 and line 31 need a change appended to InnerTrait, but i can’t seem to get it to work.
I’m also confused now by the difference between &InnerTrait<'a> and & (InnerTrait + 'a), because the generic parameter (first) version seems to semantically indicate the same lifetime constraint?

  • So far i understand that &'container InnerTrait resolves to &'container (InnerTrait + 'container), which constraints the containing references of InnerTrait to the lifetime of 'container. This leaves the question if a generic argument is necessary on InnerTrait because of it’s trait definition.

  • I would guess there isn’t a generic parameter needed on MyTrait because the containing reference (&InnerTrait) doesn’t outlive the trait itself, but the containing references of InnerTrait (&u32) should outlive it’s lifetime. I’m stuck at writing this down in the correct syntax.

Edited: Improved grammar.


#4

Man, you got yourself into some lifetime + trait object soup! :slight_smile: I understand it’s for learning purposes though, so …

The way to get your code to compile is to:
1.

trait MyTrait<'a> { <-- add the 'a lifetime parameter to the trait
    fn get_inner<'obj>(&'obj self) -> &'obj InnerTrait<'a>;
}
impl<'ax> MyTrait<'ax> for MyStruct<'ax> { 
    fn get_inner<'obj>(&'obj self) -> &'obj InnerTrait<'ax> { <-- indicate InnerTrait's lifetime parameter as 'ax
        &self.inner
    }
}

Let’s look at the original code.

trait MyTrait {
    // Note: InnerTrait lives as long as self
    fn get_inner<'obj>(&'obj self) -> &'obj InnerTrait;
}

Fully expanded the get_inner fn is fn get_inner<'obj>(&'obj self) -> &'obj (InnerTrait + 'obj). This says the returned trait object reference does not outlive 'obj - it’s tied to that lifetime.

Let’s look at:

impl<'ax> MyTrait for MyStruct<'ax> {
    fn get_inner<'obj>(&'obj self) -> &'obj InnerTrait {
        &self.inner
    }
}

This is returning a reference to InnerStruct<'a>. It, in turn, implements InnerTrait as:

impl<'long> InnerTrait<'long> for InnerStruct<'long> {
    fn get_ref<'obj>(&'obj self) -> &'long u32 {
        &self.the_ref
    }
}

And this is where the compiler gets “confused”. We’re returning a &'obj (InnerTrait + 'obj). As mentioned, this says InnerTrait does not outlive 'obj. At the same time, InnerTrait impl for InnerStruct says it returns a &'long u32, which is a lifetime associated with InnerStruct value itself, and not a borrow of it (i.e. &self). That lifetime is longer than &'obj self.

The fundamental problem is that

trait MyTrait {
    fn get_inner<'obj>(&'obj self) -> &'obj InnerTrait;
}

has no other lifetimes to associate with the returned InnerTrait itself. All output lifetimes are associated with input lifetimes ('static aside), and so the output here can only be associated with 'obj. However, we want the association to be with 'ax in the actual use case.

Let me stop here. I’m not entirely certain I explained this very clearly (hopefully it’s not wrong though). If there’s something to clarify, let me know.


#5

Thank you for your reply.

I updated the playground example, see here: https://play.rust-lang.org/?gist=7f832af0ac4276ffc54ad7b33defde56&version=stable

Can I conclude that the lifetime 'a on InnerTrait has to be carried over through MyTrait, so this must be done for each different lifetime parameter for each nested trait/struct reference?

This is an explanation about &'obj InnerTrait, right? What does + 'obj then exactly say about the lifetimes of references within InnerTrait? Is +'obj only applicable to references directly returned from it’s methods AKA it doesn’t tell anything about returned references coming from nested references? -> So that’s why a generic argument is necessary to “carry the lifetime over”?

Eventually, how come that replacing &'obj (InnerTrait<'ax> + 'obj) with &'obj (InnerTrait<'ax> + 'ax) also compiles?

Next to the above i have more questions specifically about function foo and contents of main, but i’ll tinker around first.

Thank you for the help so far guys, i really appreciate it! :slight_smile:

Edit: Added forgotten playground link.


#6

If you want to allow a trait method to return references that are longer-lived than &self (or &mut self, but let’s ignore this for now), then I think you need to define a trait-level lifetime parameter. Otherwise there’s no other lifetime you can attach to the returned reference. You can’t add another lifetime parameter to the trait methods themselves because those are caller-provided, and can’t express the lifetime of something the trait impl owns.

Note also that this is needed when talking about references from trait objects, such as in your example; you have functions returning trait objects, and the underlying struct implementing the trait is type erased; all reasoning about lifetimes after erasure is done at the trait level, and so lifetime parameters are important. If you’re dealing with concrete structs then those already carry lifetime parameters (for structs that have references) and so compiler can see what’s what in terms of lifetimes.

+ 'obj says the trait object doesn’t outlive the 'obj lifetime. That implies that any references contained in the impl behind the trait object are valid for at least 'obj. But that’s pretty much all it says about the underlying impl. In particular, it doesn’t say anything (or rather, does not relate) about lifetimes of references returned from the trait object itself.

I hope the above explanation of what + 'obj helps to answer this. For &'obj (InnerTrait<'ax> + 'obj) case, think about how you can hold a reference to something for a shorter lifetime than its scope (i.e. the underlying impl is valid for longer than the borrow lifetime). The crucial piece, though, is if you want to get a reference with 'ax lifetime out of InnerTrait then you need to call a method on the trait where that 'ax lifetime is associated with the output reference. Otherwise the only lifetimes you have at hand are the ones attached to &self, and those are typically less than the scope of the impl even, let alone any references it may hold.

Hopefully I didn’t misstate anything :slight_smile:. As before, let me know if I failed in explaining something.


#7

Hi @vitalyd

This helped me so much. I had the documentation interpreted in a way that the difference between the trait object annotation and generic parameters became unclear. I’m glad that’s cleared up.

Thanks for the help guys, i’ll go tinker some more now :slight_smile: