Return `impl Trait` in struct method vs trait method

Earlier today I rewrote a piece of old Rust code just to be confused by a compile error that did not exist in the old version of the code.

I've reduced it to the following minimal example. Basically, I want to return an impl trait from a method. In the old code this was the return type for a trait method (Factory::simple_trait() in the example). And this compiles without any errors.

In the updated code I wanted to get rid of the trait and moved the method to be directly on the struct impl (SimpleFactory::create_simple_trait() in the example). But this fails with the following compile error:

hidden type for `impl SimpleTrait` captures lifetime that does not appear in bounds

So I'm confused since the implementation code for Factory::simple_trait() and SimpleFactory::create_simple_trait() is exactly the same. The one is just an impl trait for struct while the other is an impl struct.

That raises the question: how are these two different? I suspect the trait version is doing something special with the reference to &self but am not sure what it is.

use std::cell::OnceCell;

fn main() {
    let factory = SimpleFactory::new();
    let _simple_trait = factory.simple_trait();
}

trait Factory {
    fn simple_trait(&self) -> impl SimpleTrait;
}

struct SimpleFactory {
    simple_trait: OnceCell<SimpleStruct>,
}

impl Factory for SimpleFactory {
    fn simple_trait(&self) -> impl SimpleTrait {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

impl SimpleFactory {
    pub fn new() -> Self {
        Self {
            simple_trait: OnceCell::new(),
        }
    }
    
    pub fn create_simple_trait(&self) -> impl SimpleTrait {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

trait SimpleTrait {}

struct SimpleStruct;

impl SimpleTrait for &SimpleStruct {}

On playground

Related. Your code will compile in Edition 2024.

1 Like

In the meanwhile, you can use the "Captures trick" from the other thread, or even the + '_ hint/suggestion (as in this case[1] there's no other generics that "inherit" the lifetime bound in a troublesome way).


  1. or the shared code anyway ↩︎

Ah, that explains it. Thanks!

I just don't quite understand how the trait impl captures the lifetimes. So does in give both &self and the return type the same 'a lifetime?

Rust does a lot of lifetime elision so that your code isn't cluttered with lifetime annotations, but the elision rules are chosen to either be very common patterns, or to ensure that you're more likely to get an error than to get something that works by mistake.

Writing out the relevant code without lifetime elision gets you:

trait Factory {
    fn simple_trait<'this>(&'this self) -> impl SimpleTrait + 'static;
}
impl Factory for SimpleFactory {
    pub fn simple_trait<'this>(&'this self) -> impl SimpleTrait + 'static {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

And now you have a problem - your return from self.simple_trait.get_or_init has a lifetime bound of 'this, but you need it to be 'static because that's the assumed elided lifetime.

The fix is to tell Rust that we want it to deduce the lifetime, rather than assume 'static as per the elision rules:

trait Factory {
    fn simple_trait(&self) -> impl SimpleTrait + '_;
}
impl Factory for SimpleFactory {
    pub fn create_simple_trait(&self) -> impl SimpleTrait + '_ {
        simple_trait.get_or_init(|| SimpleStruct)
    }
}

If you write this out without lifetime elision, you get:

trait Factory {
    fn simple_trait<'this>(&'this self) -> impl SimpleTrait + 'this;
}
impl Factory for SimpleFactory {
    pub fn create_simple_trait<'this>(&'this self) -> impl SimpleTrait + 'this {
        simple_trait.get_or_init(|| SimpleStruct)
    }
}

Lifetime elision in the form of -> impl Trait does not use bounds. That is, there's no desugaring or notional desugaring that corresponds to + '_ or + '_some_lifetime. And you generally don't want them to, as discussed in the other thread and its links.

The desugaring can be described in terms of precise capturing, which is another unstable feature. It would be something like this.[1]

trait Factory {
    // Note that this does not require the return outlives `'s`
    fn simple_trait<'s>(&'s self) -> impl use<'s, Self> SimpleTrait;
}

impl Factory for SimpleFactory {
    fn simple_trait<'s>(&'s self) -> impl use<'s> SimpleTrait {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

// n.b. edition 2021 with precise capturing
impl SimpleFactory {
    pub fn create_simple_trait(&self) -> impl use<> SimpleTrait {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

Until precise capturing, there's no desugaring to surface level Rust code, since there's no way to make GATs unnameable or to name closure types in your return type, etc. That said, the notional desugaring is somewhat close to...

trait Factory {
    // Note that this does not require `UnnameableGat<'a>: 'a`
    type UnnameableGat<'a>: SimpleTrait;
    fn simple_trait<'s>(&'s self) -> UnnameableGat<'s>;
}

impl Factory for SimpleFactory {
    // type UnanameableGat<'a> = impl SimpleTrait; // 'a is in scope
    type UnnameableGat<'a> = CompilerProvidedBasedOnMethodBody<'a>;
    fn simple_trait<'s>(&'s self) -> UnnameableGat<'s> {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

// n.b. edition 2021
// type SimpleFactoryCreate = impl Trait; // No lifetime in scope
type SimpleFactoryCreate = CompilerProvidedBasedOnMethodBody;
impl SimpleFactory {
    pub fn create_simple_trait(&self) -> SimpleFactoryCreate {
        self.simple_trait.get_or_init(|| SimpleStruct)
    }
}

But the GAT/type aliases are unnameable, invariant in their parameters, and do not normalize (yet still leak auto traits).


As said before it probably doesn't matter for the OP code (+ '_ is probably fine given the concrete type), but don't get the idea that elision adds + '_ bounds. Those are something different than "capturing". If you need to adjust elided capturing in the future, precise capturing (use<..>) will be the way.


  1. There may be syntactical differences from whatever is actually stabilized. ↩︎

2 Likes

Thanks a million for all the links!! Especially since I'm trying to understand the internals of Rust with this rather than just applying a quick fix to my problem.

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.