rustclosureslifetimeborrow-checkermutable-reference

Rust function pointer seems to be treated as stateful by borrow checker


Following sample code doesn't compile:

fn invoke(i: i32, mut f: impl FnMut(i32)) {
    f(i)
}

fn main() {
    let f: fn(i32, _) = invoke;

    let mut sum: i32 = 0;
    for i in 0..10 {
        _ = f(i, |x| sum += x);
    }

    println!("{:?}", sum);
}

The compiler returns following error:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `sum` as mutable more than once at a time
  --> src/main.rs:10:18
   |
10 |         _ = f(i, |x| sum += x);
   |             -    ^^^ --- borrows occur due to use of `sum` in closure
   |             |    |
   |             |    `sum` was mutably borrowed here in the previous iteration of the loop
   |             first borrow used here, in later iteration of loop

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` due to previous error

If I move f assignment to the for loop, the code compiles:

fn invoke(i: i32, mut f: impl FnMut(i32)) {
    f(i)
}

fn main() {
    let mut sum: i32 = 0;
    for i in 0..10 {
        let f: fn(i32, _) = invoke;
        _ = f(i, |x| sum += x);
    }

    println!("{:?}", sum);
}

I'm confused why the first code doesn't compile. The variable f is of type fn, which means that it is stateless. Variable f is also immutable, so even if its type were stateful, it could not store the closure. Consequently, the compiler should be able to conclude that the closure will be dropped before the next iteration of the for loop. Yet the compiler behaves as if f were mutable and it could store the closure. Could you please explain why the compiler behaves this was.

rustc version: Stable v1.68.2


Solution

  • I believe the issue is due to the implied lifetime present in the f parameter. It's as though you'd written this:

    fn invoke<'a>(i: i32, mut f: impl FnMut(i32) + 'a) {
        f(i)
    }
    

    When you store the function outside of the loop, the compiler must choose a single lifetime that applies for all invocations in the whole function.

    (Another way of looking at it is that the concrete type of this parameter will be an anonymous struct, like struct AnonymousType<'a>, implementing FnMut(i32). The important thing is that, no matter how you look at it, the concrete type deduced to satisfy impl FnMut(i32) will contain a lifetime because sum is captured by reference in the closure.)

    The lifetime cannot be restricted to a single iteration of the loop, because that lifetime would not apply to all other iterations. Therefore, the compiler must choose a longer lifetime -- but then this causes problems with the exclusive borrows overlapping, which is what you are observing.

    Moving the let f line into the loop allows the compiler to select a different lifetime for each iteration, because a different f comes into existence each iteration as well.

    Note in particular that function pointers and closures in Rust are not currently permitted to be generic, so f can't contain a function pointer that's generic over the hidden lifetime. That feature could be added later, and if it is then that would allow this code to compile.