Why isn't the `+ 'static` on my `-> impl Future<...> + 'static` trait method respected?

I have a trait function which takes a reference to self and returns a 'static future. But I can't seem to use it. The compiler still insists that my future captures the lifetime of self even when it clearly doesn't:

use std::future::Future;
  
trait Foo: 'static {
    fn bar(&self) -> impl Future<Output = Bar> + 'static;
}
  
struct Bar;
  
struct Baz;
  
fn bar_to_baz(Bar: Bar) -> Baz {
    Baz
}
  
fn baz<T: Foo>(t: &T) -> impl Future<Output = Baz> + 'static {
    let fut = t.bar();
    async move { bar_to_baz(fut.await) }
}

results in

error[E0700]: hidden type for `impl Future<Output = Baz> + 'static` captures lifetime that does not appear in bounds
  --> src/main.rs:17:5
   |
15 | fn baz<T: Foo>(t: &T) -> impl Future<Output = Baz> + 'static {
   |                   --     ----------------------------------- opaque type defined here
   |                   |
   |                   hidden type `{async block@src/main.rs:17:5: 17:41}` captures the anonymous lifetime defined here
16 |     let fut = t.bar();
17 |     async move { bar_to_baz(fut.await) }
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

async functions capture all their generic inputs at the (opaque) type level, and the workaround[1] of capturing less due to things like bounds that can only be met for one specific generic input (like your + 'static) never materialized.[2]

You want precise capturing, but we don't have it yet.

The workaround on stable is to use an associated type.

trait Foo: 'static {
    type Fut: Future<Output = Bar> + 'static;
    fn bar(&self) -> Self::Fut;
}

Most (stable) implementors today will have to type-erase and return e.g. Pin<Box<dyn Future>> or more auto-trait laden versions,[3] but eventually impl Trait for associated types will also become possible, at which point the above will be about equivalent to precise capturing.[4]


  1. or arguably hack ↩︎

  2. Technically your bound is respected, but the only way to meet it is to pass in a &'static self. ↩︎

  3. because most futures are not nameable ↩︎

  4. but also make the return type indirectly nameable via the associated type ↩︎

3 Likes

Are you saying there's an implicit + '_ on the future so when I add + 'static, rather than overriding the implicit lifetime constraint, it combines with the constraint to be + '_ + 'static and so the + 'static is redundant and doesn't help?

Aren't there a lot of places in rust where the implicit lifetime can be overridden by explicitly giving the lifetime as 'static? If so, why not here as well?

Because an async block (in the general case) necessarily has to capture some local state.

It is the same reason why the following doesn't compile:

let local: usize = 0;
let ptr: &'static usize = &local;

I.e., you can't make stuff live longer merely by using lifetime annotations. Lifetime annotations are for describing constraints and letting the compiler check their correctness. The only way you can make values live longer is by moving their bindings to larger scopes, but due to the way async data is defined to be translated into low-level memory operations, that's simply not possible.

1 Like

I think there's a misunderstanding here. It's an async move block, not just an async block and also I deleted the comment I think you're responding to

Captures and + '_ aren't exactly the same thing. But if you have worked with -> impl Trait outside of traits, I definitely understand why you have that intuition. It is the case that -> impl Trait in traits implicitly captures all generics, including all lifetimes.[1]

In contrast, -> impl Trait outside of traits does not implicitly capture lifetime generics unless you add something like + '_. Until the next edition, where they'll act like -> impl Trait in traits.


-> impl Trait implicitly defines an opaque type, or more properly, an opaque type constructor (parameterized by the generics it captures).

"Capturing" means that the opaque type can be defined in terms of the captured parameter in some way, like the parameters on a generic associated type. Like with a GAT, it doesn't have to use the parameters, but it is allowed to. Also like a GAT, any bounds also have to be met. Unlike a GAT, opaque types cannot be normalized, so outside consumers must always act like an opaque type uses all of its parameters.

+ '_ is a bound, not a capture. However, outside of traits, on the current edition, -> impl Trait doesn't capture lifetimes by default. But they are captured if a lifetime is mentioned in a bound.

That's a lot of words, so let's see some code:

trait Example {
    fn some_trait_method<T>(&self) -> impl Trait + 'static;
}
trait Example {
    // Every `+` separated thing after `-> impl` became a bound
    opaque type StmOut<'a, T>: Trait + 'static;
    fn some_trait_method<T>(&self) -> Self::StmOut<'_, T>;
}

Without the + 'static, the lifetime is still captured.

trait Example {
    // fn some_trait_method<T>(&self) -> impl Trait;

    opaque type StmOut<'a, T>: Trait;
    fn some_trait_method<T>(&self) -> Self::StmOut<'_, T>;
}

But that's not that same as "an implicit + '_":

trait Example {
    // fn some_trait_method<T>(&self) -> impl Trait + '_;
    // fn some_trait_method<'s, T>(&'s self) -> impl Trait + 's;
    //                               vvvv                  ^^^^
    opaque type StmOut<'a, T>: Trait + 'a;
    fn some_trait_method<T>(&self) -> Self::StmOut<'_, T>;
}

Adding + '_ resulted in a new bound on the opaque type, which in turn requires T: 'a in the opaque type. That wasn't required when + '_ wasn't present. And often this isn't what you want, in which case implicit capturing is superior, assuming you do need to capture the lifetime.

For that reason, and consistency,[2] -> impl Trait outside of traits will also capture all generics in the next edition. And we'll get precise capturing for backwards compatibility.

There was a plan to implicitly drop the lifetime capturing sometimes if a + 'static implied a lifetime parameter couldn't be used, but it didn't pan out apparently; hence precise capturing.

Here's another (contrived) example where a bound requires a lifetime parameter to be 'static...

struct Foo;
impl Trait for &'static Foo {}
impl Foo {
    fn example<'s>(&'s self) where &'s Self: Trait {}
}

...but that doesn't mean example isn't parameterized by a lifetime. That's sort of like what's going on in your OP.


  1. So do async fn, in or out of traits. ↩︎

  2. and async being the hype feature of the decade, ↩︎

1 Like

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.