What precisely does a placeholder lifetime mean in an async closure signature?

Let's say I have a struct like this that accepts a lifetime parameter:

struct S<'a>(&'a str);

What does the lifetime parameter mean in this closure?

async |_: S<'_>| { todo!() }

Given that underneath this expands to an async_call_mut method, I thought maybe it meant that the S parameter needs to have the same lifetime as the closure borrow. But I guess that doesn't make sense in the context of AsyncCallOnce, and this program compiles fine despite the closure not being 'static (playground):

struct S<'a>(&'a String);

fn check_accepts_static<F>(_: &F)
where
    F: AsyncFnMut(S<'static>),
{
}

fn main() {
    let f = async |_: S<'_>| todo!();
    check_accepts_static(&f);

    let s = String::from("taco");
    f(S(&s));
}

So I now suspect that what's going on here is that the closure is generic over the lifetime associated with S. I can't find any references on this—the internet says that closures cannot be generic, but perhaps that means "aside from lifetimes".

Is it accurate to say that there is not one AsyncFnMut implementation, but a family of them, and they wind up looking something like this (particularly with respect to the lifetimes and their relationships to each other)?

struct SomeClosure;
struct FnMutFuture<'c, 's>(&'c mut SomeClosure, S<'s>);

impl<'s> AsyncFnMut<(S<'s>,)> for SomeClosure {
    type CallRefFuture<'c> = FnMutFuture<'c, 's>;

    extern "rust-call" fn async_call_mut(&mut self, args: (S<'s>,)) -> Self::CallRefFuture<'_> {
        FnMutFuture(self, args.0)
    }
}

(Playground)

I really wish there were a way to get the compiler to dump something like this for its own generated implementations, but I can't find one.

It means "this closure accepts a S<'_> with any lifetime". With no annotation:

    let f = async |arg| { let _: S = arg; };

the closure would only accept S<'inferred> for one particular lifetime, inferred elsewhere. It's a well-known and painful shortcoming of closure inference.

(There's only one implementation, but it's generic. And note how SomeClosure is not generic either.)

For the given example, FnMutFuture need not hold a borrow of the closure. Note how this compiles but this does not. A different closure body could hold a borrow of the closure, though. (That was a motivation for adding AsyncFn* on top of Fn*.)

Agreed!

1 Like

You're right, thanks for correcting those details. It sounds like you're agreeing, but just to check explicitly: you agree that the shape of the AsyncFnMut implementation in the original post is the correct one for a closure that does capture something and so does need to be borrowed by the future?

There might be some async related details I'm unaware of, like maybe some fields get pinned or something? But generally speaking, like how the lifetimes work out, I agree with the shape in the OP.

Thanks!