Why does precise capture in a trait method require capturing Self?

Why do precise capture lists force you to include Self for a trait? In this example the code doesn't compile because DF is deduced to need to be 'static:

trait DoFoo {
    fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> impl use<'a, Self> + Sized;
}

fn use_do_foo<DF: DoFoo>(df: &DF, n: &'static i32) {
    check_static(&df.do_foo(n));
}

fn check_static<T: 'static>(t: &T) {}
error[E0310]: the parameter type `DF` may not live long enough
 --> src/lib.rs:8:5
  |
8 |     check_static(&df.do_foo(n));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |     |
  |     the parameter type `DF` must be valid for the static lifetime...
  |     ...so that the type `DF` will meet its required lifetime bounds

That makes sense because of the use<'a, Self>. But I don't want the Self there; the compiler forces me to add it:

error: `impl Trait` must mention the `Self` type of the trait in `use<...>`
 --> src/lib.rs:4:50
  |
3 | trait DoFoo {
  | ----------- `Self` type parameter is implicitly captured by this `impl Trait`
4 |     fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> impl use<'a> + Sized;
  |                                                  ^^^^^^^^^^^^^^^^^^^^
  |
  = note: currently, all type parameters are required to be mentioned in the precise captures list

Is there a way for a trait method to return impl Trait with an associated lifetime without incorporating Self's lifetime into it?

You can probably just add an outlives bound, right?

impl use<'a, Self> + 'a + Sized;

Self is needed because the impl-trait return type can obviously always depend on Self, given that each implementation of DoFoo can choose its own type to use for returning.

You’re trying to express something like “the type may depend on Self but not make use of lifetimes in Self”, which isn’t really directly expressible; but in this case just demanding it does fulfill the : 'a bound is pretty much the next best thing.

For notational simplicity, since I don’t believe it really makes much of any difference, you could also then just write

impl 'a + Sized

since I don’t think the capture of 'df has much of an effect in the presence of an : 'a bound. (Though I’d be happy to learn more, if anyone knows of any subtle effects, and thus arguments in favor of choosing one or the other between impl use<'a, Self> + 'a + Sized and impl use<'a, 'df, Self> + Sized, the latter being equivalent to impl 'a + Sized in edition 2024 and onwards.)


edit: okay it looks like they’re not fully intercompatible, so I’d choose to probably leave off the 'df usage and go with the 'a + use<'a, Self> approach which can be nicer for any downstream uses of the abstract type :slight_smile:

1 Like

See here for an example of how logically outlived bounds cannot be entirely erased. That PR was superseded by this one and now the lifetime is still captured "by name", but not "materially" (to make up a some terminology on the spot) -- it doesn't participate in liveness tracking for example.

Some people find this disturbing, but my conclusion from participating in that issue is that

  • If PR 116040 is problematic then so is outlives bounds taking precedence more generally (which has been around longer), because that allows erasing lifetimes (e.g. via a dyn Any roundtrip)

  • The current behavior is consistent with the rigid associated type model of opaques; that is, if you have a

    pub trait AssocTyModel<'r> {
        type Opaque: 'static + Into<&'r str>;
        fn define(&self, r: &'r str) -> Self::Opaque;
    }
    

    then uses of T::Opaque in a generic (non-normalizable) setting also don't participate in liveness checking etc. (and this has been true forever)

1 Like

Huh, indeed I can, thanks. I guess I didn't try that because I don't really understand why it works. I was already explicitly capturing 'a, right?

Maybe I should have asked for an explanation. Why am I getting the error in the original post, and why does your suggeston fix it?

Right, but capturing is not the same as imposing an outlives bound, which is what + 'a does. The outlives bound means the type as a whole meets the bound. There's more discussion in this post.

It's possible that an implementator that doesn't meet a 'a bound returns something that doesn't meet a 'a bound, like &'x () returns itself.

(That's also related to why adding a bound to the implementor on use_do_foo, like the compiler suggests, fixes the error: it becomes impossible for anything that doesn't meet a 'a = 'static bound to be part of the returned type. It's analogous to how outlives of associated types works.)

Now the return type is required to meet the bound (so &'x () wouldn't be allowed to return itself in its implementation).

Thanks, that's a very good post.

Quick side rant: I wish this stuff were documented somewhere central. As a newcomer to the language it's really disappointing that understanding any particular advanced feature requires sifting through out of date blog posts and RFCs that use syntax that wasn't implemented full of acronyms you don't know and references to proposals you're unfamiliar with, trying to piece together the puzzle from deltas relative to versions of the language you've never used. :-/ The libraries are fantastically documented, but the language itself rather poorly so.

But what 'a bound? Why is the lifetime of the implementor relevant at all once its do_foo method returns?

Originally I thought that use<'a> implied an 'a bound, but then the answer given above seems to imply it doesn't, and so I'm really confused on this point. Are you saying that in the original version there is some 'a bound on the result of DoFoo::do_foo, despite the fact that adding an explicit 'a bound in this version is what fixes the problem?

I meant, you have this method:

fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> impl use<'a, Self> + Sized;

And elsewhere you want the return type to meet an outlives bound that corresponds to 'a in that signature:

fn use_do_foo<DF: DoFoo>(df: &DF, n: &'static i32) {
    fn check_static<T: 'static>(t: &T) {}
    check_static(&df.do_foo(n));
}

But consider this implementation:

struct DownStreamSomewhere<'a>(&'a str);
impl<'this> DoFoo for DownStreamSomewhere<'this> {
    fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> impl use<'a, 'this> + Sized {
        Self(self.0)
    }
}

The use<'a, ..> does not impose an 'this: 'a bound on 'this (nor vice-versa[1]). So you can pass in a &'static i32 and get back an arbitrarily short DownStreamSomewhere<'this>.

Therefore this is a concrete example of something which satisfies DF: DoFoo but fails your 'static check. Since it's possible, and use_do_foo is required to work with any DF: DoFoo, putting your 'static check within use_do_foo results in the borrow check error.

I agree on this point. I hope the specification project improves matters eventually. :crossed_fingers:

In case it helps, let me outline how I think of -> impl Trait.

fn example<'a, 'b, T>() -> impl use<'a, T> + Trait + Send { ... }

Corresponds to

//               vvvvv generics of `+ use<..>`
trait ExampleRet<'a, T> {
    #[rustc_opaque] // <-- forbid knowing ("normalizing") the type everywhere...
    #[rustc_leak_auto] // <-- ...except details about the auto traits
    type Ty: Trait + Send;
    //       ^^^^^^^^^^^^ Bounds on return type (including outlives, if any)
}

struct HiddenType;
impl ExampleRet<'a, T> for HiddenType { ... }
fn example<'a, 'b, T> -> <HiddenType as ExampleRet<'a, T>>::Ty { ... }

And in a trait it would be more like

trait MethodHolder<'m, 'n, M> {
    // You're forced to capture all trait parameters and `Self`
    fn method<'a, 'b, T> -> impl use<'a, 'm, 'n, M, T, Self> + Trait + Send;
}

Becomes

trait MethodHolder<'m, 'n, M> {
    //      vvvvv Captured generics from the *method*
    type Ty<'a, T>: Trait + Send; // <-- Bounds (including outlives, if any)
    fn method<'a, 'b, T> -> <Self as MethodHolder<'m, 'n, M>>::Ty<'a, T>;
    //                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // Note how all *trait parameters and `Self`* appear in this path
}

And the definition is based on each implementation. Note how when defining the associated type,[2] it's possible for the implementor to name all the trait parameters and any generics that might be part of Self.

In terms of your OP and my DownStreamSomewhere example, that would be:

struct DownStreamSomewhere<'a>(&'a str);
impl<'this> DoFoo for DownStreamSomewhere<'this> {
    type FooRet<'a> = DownStreamSomewhere<'this>;
    fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> Self::FooRet<'a> {
        Self(self.0)
    }
}

There's nothing preventing this implementation which defines a return type FooRet<'a> that doesn't have to outlive 'a... unless you add + 'a or similar.

 trait DoFoo {
-    type FooRet<'a>: Sized;
+    type FooRet<'a>: Sized + 'a;
     fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> Self::FooRet<'a>;
 }

I.e.

 trait DoFoo {
-    fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> impl use<'a, Self> + Sized;
+    fn do_foo<'a, 'df>(&'df self, n: &'a i32) -> impl use<'a, Self> + Sized + 'a;
 }

If you understand how associated types work well enough -- for example, how "projection outlives" works from that RFC I linked -- I find this way of thinking about things to be quite useful when reasoning about -> impl Trait.

With all that context, I hope it's more clear that use<'a> is about lifetimes that can be mentioned in the definition of the hidden type of an opaque, but doesn't impose a bound on its own.

Similarly, if you have a lifetime parameter on a trait that has an associated type, an implementation can use that parameter in the definition of the associated type, but doesn't have to. And whether it does or doesn't, the mere presence of the lifetime doesn't impose an outlives bound on the associated type.


  1. if every captured lifetime was a bound on the others, all captured lifetimes would have to be the same ↩︎

  2. any associated type actually ↩︎

Fantastically helpful reply, thank you. That kind of thing is exactly what I wish there were central, up-to-date documentation showing.

I think I see what you're saying now. In fact that way of thinking about it was basically my mental model originally (just much more precise). I was confused by the error, which I incorrectly concluded meant that the lifetime of Self was automatically tied into things by virtue of being mentioned by use, so then I concluded the same must be true of 'a.

In fact I guess what's going on is what you've been trying to explain: some implementation of DoFoo could return a reference with whatever lifetime Self has because the trait leaves this unspecified, therefore in order for use_do_foo to be able to compile it would need to know that DF is 'static because if it is then this would present no problem. I'm actually really impressed that the compiler is able to infer that far "backwards".