Search code examples
rust

Using enumerate with async blocks


I have the following Rust code

use std::error::Error;

#[derive(Debug)]
enum MyEnum {
    First,
    Second,
}

fn do_something(index: usize, m: &MyEnum) {
    eprintln!("do_something: {} {:?}", index, m);
}

async fn test() -> Result<(), Box<dyn Error>> {
    let myvec = vec![MyEnum::First, MyEnum::Second];
    let c = String::from("cap");

    let futures = myvec.iter().enumerate().map(|(index, el)| async {
        eprintln!("Element: {}", &c);
        do_something(index, el);
    });

    futures::future::join_all(futures).await;
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    test().await?;
    Ok(())
}

Compiler complains that:

error[E0373]: async block may outlive the current function, but it borrows `index`, which is owned by the current function
  --> src/main.rs:17:62
   |
17 |     let futures = myvec.iter().enumerate().map(|(index, el)| async {
   |                                                              ^^^^^ may outlive borrowed value `index`
18 |         eprintln!("Element: {}", &c);
19 |         do_something(index, el);
   |                      ----- `index` is borrowed here
   |

Why it's not complaining about the el variable?

And how to fix this code? I would like to keep using the iterator and not for loop. I can change other parts of code, e.g. do_something() can have different prototype.

I've put the c variable here to forbid the easy solution with async move {...}. The string c should not be moved.

Without enumerate() (and without index) it works. I would expect that index (which has usize type) is easily copy-able.


Solution

  • I would expect that index (which has usize type) is easily copy-able.

    That's exactly the problem, because usize is copy, for it to move the compiler only needs to capture a shared reference it can just copy the bytes when it needs a value. It'll use the weakest kind of capture it can, first &, then &mut and only if these don't work it'll move.

    The problem is that index is a local to the closure, which is gone when it returns the future.

    el doesn't have that same problem because it is a reference to the outer myvec which is owned by test, which does live long enough, hence any references to it also can live long enough.

    To fix it you simply apply the "easy solution" you found yourself, any captures that you do not want to move you can simply shadow1 with the desired binding kind (& or &mut):

    async fn test() -> Result<(), Box<dyn Error>> {
        let myvec = vec![MyEnum::First, MyEnum::Second];
        let c = String::from("cap");
    
        let futures = myvec.iter().enumerate().map({
            let c = &c;
            move |(index, el)| async move {
                eprintln!("Element: {c}");
                do_something(index, el);
            }
        });
    
        futures::future::join_all(futures).await;
    
        eprintln!("c is still available: {c}");
        Ok(())
    }
    

    1 of course you don't have to shadow it, but shadowing allows you to not worry about a new name and is quite idiomatic here.