Nested FnMut "returns a reference to a captured variable which escapes the closure body"

Viewed 46

Let's say I'm using a nested closure to modify a local variable like so:

    let mut i = 0;

    let x = (0..).flat_map(|_| {
        (0..).map(|_| {
            let x = i;
            i += 1;
            x
        })
    });

This does not compile with the error:

error: captured variable cannot escape `FnMut` closure body
  --> src/main.rs:35:9
   |
32 |       let mut i = 0;
   |           ----- variable defined here
33 |
34 |       let x = (0..).flat_map(|_| {
   |                                - inferred to be a `FnMut` closure
35 | /         (0..).map(|_| {
36 | |             let x = i;
   | |                     - variable captured here
37 | |             i += 1;
38 | |             x
39 | |         })
   | |__________^ returns a reference to a captured variable which escapes the closure body
   |
   = note: `FnMut` closures only have access to their captured variables while they are executing...
   = note: ...therefore, they cannot allow references to captured variables to escape

I experimented with the same code on a non-nested context, and it compiles without an error:

    let mut i = 0;

    let x = (0..).map(|_| {
            let x = i;
            i += 1;
            x
    });

So I guess the error comes from the fact that the closure is nested, but I can't fully figure out why the error is being triggered.

1 Answers

Let's strip out all of the iterator parts and consider just the closures that are being constructed.

    let mut i = 0;
    let f = || {
        || {
            let x = i;
            i += 1;
            x
        }
    };

i is a variable being used by the inner closure, and therefore borrowed by it. Now, think about what happens if I run this program:

let c1 = f();
let c2 = f();
[c1(), c2()]

Both c1 and c2 are closures that capture i and mutate it. Therefore, having both of them allows shared mutable state without any synchronization/mutual-exclusion mechanism, which is always prohibited in Rust. Compiling the above code will produce the same error as you had.

Iterator::flat_map() won't actually call the function and keep two of it around like this, but its signature can't communicate that fact to the compiler, so the compiler has to assume the worst could happen.


Now that we know that it would be unsound to permit this pattern, why is the error described the way you saw?

When you refer to a variable in a closure, this (most often) becomes a borrow of that variable. In other words, calling || { i += 1; } constructs a value that contains an &mut i32 pointing to i. This means that || { i += 1; } itself is an expression that needs to be able to mutably borrow i, so a closure which contains || { i += 1; } itself requires a mutable borrow of i.

Whenever a closure contains a mutable borrow (or some other cases), that means that invoking the closure itself requires &mut self (on the same general principle that you can't ever mutate an &mut by way of an &). That's why the closures are inferred to be FnMuts (the type of function that requires &mut self to invoke) and the compiler is telling you about that.

Now, why do FnMut closures “only have access to their captured variables while they are executing”? I'm not sure how to explain this exact description of the problem, but I'm sure that it's fundamentally about the requirement of &mut being unique, and that if it were permitted there would end up being some way to modify the function's state while its result was still borrowed.


Finally, how do you solve your problem? Well, in simple cases, the answer is usually to make the closure a move || { closure. That means there is no longer a borrow, and the closure now owns the i counter, so it can live as long as the closure does. But in your case, this doesn't help, because you're actually trying (I assume) to have a counter which is shared between everything. Therefore you need an interior mutability tool. The simplest one for this situation is Cell, which allows reading and writing a value (as long as it's Copy) without ever borrowing it.

    use std::cell::Cell;

    let i = Cell::new(0);
    let x = (0..).flat_map(|_| {
        (0..).map(|_| {
            let x: i32 = i.get();
            i.set(x + 1);
            x
        })
    });
Related