Thanks for the crystal clear explanation!
For posterity, I want to complete.
Here async is not needed to reproduce the problem I had. As async block capture mode works the same way as closure, we can have a reproducer with closures only:
fn main() {
for_each_city(|city| {
// Doesn't compile.
// Box::new(|| println!("{city}"))
// But with reborrowing it does.
Box::new(|| println!("{}", &*city))
});
}
fn for_each_city<F>(f: F)
where
F: for<'c> Fn(&'c str) -> Box<dyn Fn() + 'c>,
{
for x in ["New York", "London", "Tokyo"] {
f(x)();
}
}
I found it easier to reason about the code once we remove the async part of the equation.
We can even simplify it further and get the same compile error with this code, which also only compiles with reborrowing:
fn f(s: &String) -> impl FnOnce() {
|| {
s;
}
}
So why ?
I will try to desugar this version (f1) and the reborrowed version (let's call it f2), based on the explicit capture clauses blog post. I use the 2024 edition, where return impl trait captures everything by default.
pub fn f1_desugared<'a>(s: &'a String) -> impl FnOnce() + use<'a> {
struct ClosureOne<'a, 'b> {
s_ref: &'b &'a String,
}
impl<'a, 'b> FnOnce<()> for ClosureOne<'a, 'b> {
type Output = ();
extern "rust-call" fn call_once(self, _args: ()) -> () {
self.s_ref;
}
}
ClosureOne { s_ref: &s } // borrowed value does not live long enough
}
ClosureOne borrows the reference &'a String that will be dropped at the end of the function, causing the error.
fn f2<'a>(s: &'a String) -> impl FnOnce() + use<'a> {
|| {
&*s;
}
}
pub fn f2_desugared<'a>(s: &'a String) -> impl FnOnce() + use<'a> {
struct ClosureTwo<'a> {
s_ref: &'a String,
}
impl<'a> FnOnce<()> for ClosureTwo<'a> {
type Output = ();
extern "rust-call" fn call_once(self, _args: ()) -> () {
self.s_ref;
}
}
ClosureTwo { s_ref: &*s }
}
The ClosureTwo directly borrows the string, so it doesn't matter that s is dropped at the end of the function.
Do you agree with my desugaring ?
Note about debugging closure capturing: Rust analyzer can show on hover information about closure capturing, which is really handy. There is also this option. Sadly none of them can currently show the capture of async closure or async block, so I found the usage of this macro pretty handy:
#![feature(rustc_attrs)]
#![feature(stmt_expr_attributes)]
#![expect(internal_features)]
pub fn f(s: &String) -> impl FnOnce() {
#[rustc_capture_analysis] || {
s;
}
}
The information of closure/async block capture seems to also be on the thir and mir representation, but it's more cumbersome to analyze.