Closure that borrow from their captures

I'm experimenting with closures that return a Future, passed as a function parameter. If I create the Future with an async {} (without the move), I get a compile error because closure can't borrow from their capture. It was one of the motivations for the async closure.

But if I reborrow inside the async {}, it compiles. Why this is working ?

use std::pin::Pin;

#[tokio::main]
async fn main() {

    // Doesn't compile.
    // for_each_city_box(|city| Box::pin(async { println!("{city}") })).await;

    // But with reborrowing it does.
    for_each_city(|city| {
        Box::pin(async {
            let city = &*city;
            println!("{city}")
        })
    })
    .await;
}

async fn for_each_city<F>(mut f: F)
where
    F: for<'c> FnMut(&'c str) -> Pin<Box<dyn Future<Output = ()> + Send + 'c>>,
{
    for x in ["New York", "London", "Tokyo"] {
        f(x).await;
    }
}

Playground link

println!() and other formatting macros implicitly borrow the expressions used in them, so when you don’t write the reborrow, the println!() borrows the outer city from |city| {, creating a reference of type &'1 &'c str, where '1 is a lifetime that can’t outlive the closure call stack frame.

When you write let city = &*city, since city is of type &'c str, the dereference *city is of type str, and city is a local variable of type &'2 str, where '2 is a lifetime that can’t outlive 'c. Then the println!() creates a borrow of type &'3 &'2 str, uses it, and drops it before the async block ends.

The key difference is that in the working version, you are not borrowing the variable city from |city| { — only borrowing the str that city points to.

3 Likes

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.

Yes.

1 Like