Closure captures in Rust

Viewed 81

I just found out that the following code

fn create_fn_once() -> impl FnOnce(char) -> String {
    let text = "text".to_string();  // not implement the `Copy` trait
    let y = 1;                      // implement the `Copy` trait

    |x| {
        move_x(text);   // ok
        move_x(y);      // error: `y` is borrowed here
        x.to_string()
    }
}

fn move_x<T: Sized>(x: T) {}

Fails with (playground)

error[E0373]: closure may outlive the current function, but it borrows `y`, which is owned by the current function
 --> src/lib.rs:5:5
  |
5 |     |x| {
  |     ^^^ may outlive borrowed value `y`
6 |         move_x(text);   // ok
7 |         move_x(y);      // error: `y` is borrowed here
  |                - `y` is borrowed here
  |
note: closure is returned here
 --> src/lib.rs:5:5
  |
5 | /     |x| {
6 | |         move_x(text);   // ok
7 | |         move_x(y);      // error: `y` is borrowed here
8 | |         x.to_string()
9 | |     }
  | |_____^
help: to force the closure to take ownership of `y` (and any other referenced variables), use the `move` keyword
  |
5 |     move |x| {
  |     ++++

I know move |x| { can fix it. But why is y borrowed?

1 Answers

https://doc.rust-lang.org/reference/types/closure.html#capture-modes

The compiler prefers to capture a closed-over variable by immutable borrow, followed by unique immutable borrow (see below), by mutable borrow, and finally by move. It will pick the first choice of these that is compatible with how the captured variable is used inside the closure body. The compiler does not take surrounding code into account, such as the lifetimes of involved variables, or of the closure itself.

y of type i32 implements Copy (whereas text of type String does not). Therefore, the usage of y inside the closure does not require y to be owned, because it is copied, rather than moved, for the move_x(y) call.

As the reference says, “The compiler does not take surrounding code into account” — closure captures are always based on the usage only. The compiler does not reason “this cannot live long enough, so I had better move it into the closure”. While that's inconvenient in this case, in general it would make it much harder to understand what is happening (and harder to compile), because lifetime inference would be feeding back into closure capture decisions.

(By the way, your fn move_x<T: Sized>(x: T) {} is already available in the standard library as std::mem::drop.)

Related