Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Rust not automatically move when necessary?

Tags:

rust

The following program compiles without issue:

#[tokio::main]
async fn main() {
    async fn g(x: String) {}

    let f = || {
        let y: String = String::from("a").clone();

        return async {
        println!("{}", &y);
        return g(y).await;
    }};
}

However, if the line "return g(y).await;" is removed, it will fail with the following:

error[E0373]: async block may outlive the current function, but it borrows `y`, which is owned by the current function
  --> src/main.rs:35:22
   |
35 |           return async {
   |  ______________________^
36 | |         println!("{}", &y);
   | |                         - `y` is borrowed here
37 | |         // return g(y).await;
38 | |     }};
   | |_____^ may outlive borrowed value `y`
   |
note: async block is returned here
  --> src/main.rs:35:16
   |
35 |           return async {
   |  ________________^
36 | |         println!("{}", &y);
37 | |         // return g(y).await;
38 | |     }};
   | |_____^
help: to force the async block to take ownership of `y` (and any other referenced variables), use the `move` keyword
   |
35 |         return async move {
   |                      ++++

Why does the same error not appear in the original code?

like image 664
Test Avatar asked Oct 20 '25 07:10

Test


1 Answers

Rust does the minimum amount of work necessary to get your closure to work.

let f = || {
    let y: String = String::from("a").clone();
    return async {
        println!("{}", &y);
    }
};

Here, the inner closure requires y by reference. So Rust is going to turn it into, essentially, a struct with a &String. Removing the async stuff for simplicity, it would turn this

let f = || {
    let y: String = String::from("a").clone();
    || {
        println!("{}", &y);
    }
};

into, effectively, this

struct MyCustomClosure<'a> { y: &'a String };

impl<'a> FnOnce for MyCustomClosure<'a> {
  fn call_once(self) {
    println!("{}", self.y)
  }
}

// (Same impl for FnMut and Fn ...)

let f = || {
    let y: String = String::from("a").clone();
    return MyCustomClosure { y: &y }
};

Now, sometime way later on in the compilation process, Rust realizes that the lifetime 'a for MyCustomClosure doesn't line up with the lifetime for the enclosing function, and it complains. But by this point it's already committed to using a reference here and it's not smart enough to go back and try a different closure type. It's two different stages of compilation that don't talk to each other directly.

This on the other hand

let f = || {
    let y: String = String::from("a").clone();
    || { y }
};

This, on the other hand, very clearly requires a move. We're passing ownership inside the closure, so we get a closure that only implements FnOnce and that takes the y by value. Essentially we get

struct MyCustomClosure2 { y: String };

impl FnOnce for MyCustomClosure2 {
  fn call_once(self) -> String {
    self.y
  }
}

// No FnMut or Fn this time, since we need to pass ownership of a value.

Now there's no lifetime argument 'a to cause conflicts down the road. There's just a simple struct and it all works out.

As the error message indicates, if your intent is to get an FnOnce which returns the string by moving, you can prefix your closure with the move keyword.

let f = || {
    let y: String = String::from("a").clone();
    return async move {
        println!("{}", &y);
    }
};
like image 88
Silvio Mayolo Avatar answered Oct 22 '25 05:10

Silvio Mayolo



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!