Can a closure's return value borrow from a captured variable?

Hello,
It seems to me that the following code should be sound, because f lives longer than the returned value:

let mut z = 0;
let mut f = move || {
    z += 1;
    return &z;
};
println!("{}", f());
println!("{}", f());
println!("{}", f());

Yet the compiler gives me an error:

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
   --> src/main.rs:118:16
    |
118 |         return &z;
    |                ^^
    |
note: first, the lifetime cannot outlive the lifetime  as defined on the block at 116:24...
   --> src/main.rs:116:25
    |
116 |     let mut f = move || {
    |                         ^
note: ...so that closure can access `z`
   --> src/main.rs:118:16
    |
118 |         return &z;
    |                ^^
note: but, the lifetime must be valid for the scope of call-site for function at 116:24...
   --> src/main.rs:116:25
    |
116 |     let mut f = move || {
    |                         ^
note: ...so that return value is valid for the call
   --> src/main.rs:118:16
    |
118 |         return &z;
    | 

I this sort of thing possible in current Rust?

z is not only captured but moved. That means it no longer exists when the closure exits. So you try to return a ref to a value that no longer exists, which is use-after-free. Luckily borrowck kept you from making this mistake.

You'd have another problem: f is mutable and owned, so you can call it and mutate the closure's z any time you want. But binding the returned &z doesn't count as borrowing f which means you can still call f and mutate z. So you return an immutable ref to z but that's a lie because z really is mutable and it's value can change at any time you call f() again.

That's not exactly true, &z would borrow f.z, but it's the same f.z for all 3 calls.
That is, the closure's "call" signature would be &'a mut self -> &'a i32 which looks sound to me.
What you couldn't do is [f(), f()], but calling them separately could count as individual borrows just fine.
cc @nikomatsakis

Yes, I expect that my closure should be roughy roughly equivalent to this:

struct F {
    z: i32
}
impl F {
    fn call<'a>(&'a mut self) -> &'a i32 {
        self.z += 1;
        &self.z
    }
}
let mut f = F { z: 0 };
println!("{}", f.call());
println!("{}", f.call());
println!("{}", f.call()); 

...which works just fine.

2 Likes

So what current status of this? Is there any issue on GitHub?

I don't think it's possible with how the FnMut trait is defined:

fn call_mut<'a>(&'a mut self, args: Args) -> Self::Output;

The &'a mut self argument is restricted to lifetime 'a, chosen freely by the caller. There is no way for Self::Output to declare dependence on the lifetime 'a. You can't really say type Output = &'a i32 since the 'a isn't even in scope yet!

I'm not sure what I was thinking, but I agree the current definition doesn't work with what I said. i.e. this can be sound but it requires different traits.

Okay, but where does it say that this closure must implement FnMut? Is this an implicit assumption in the compiler?

If it doesn’t implement FnMut, then there’s no way to call f() more than once. In your example, I don't think the error is not caused by FnMut; however, move causes z to be moved into the closure, and the by-value self argument causes it to be consumed. Therefore, you still can't return a reference to it.

What by-value self argument are you referring to? It seems the problem is precisely the FnMut signature and nothing else.

I mean, if the closure is move (as shown in the OP), then z would be moved into the closure and thus only FnOnce should be possible.

I think you've got this the wrong way around: FnOnce is for when a variable is moved out of the closure.

But 'z' is Copy, so the move should just copy the value into the closure, and thus "detach" them. It's also mutating the value, so FnMut is generated. Unless I'm missing something.

Edit: in fact, 'z' being Copy is irrelevant for the closure itself - that only comes into play if you want to continue using 'z' in the outer scope. I thought the whole point of move closures is to detach them from outer context. The "F" struct mentioned upthread seems like the desugaring modulo not being able to associate a self lifetime with the return value.

FnOnce is when closure consumes something it captures - simple example is iterating over a (captured) HashMap inside the closure. Or otherwise consuming a captured value, no different than consuming 'self' in other contexts.

You're right, move is actually unrelated to FnMut vs FnOnce.

This argument makes sense to me, but wouldn't the same argument apply to the Fn trait?

fn call<'a>(&'a self, args: Args) -> Self::Output;    

Yet the following code works fine:

let y = 10;
let f = || { &y };
println!("{}", f());
println!("{}", f());
println!("{}", f());

The Fn example is different because it’s returning an immutable reference, which is a Copy type. That closure is essentially:

struct __Closure<'a> {
    y: &'a i32,
}

The Output associated type in the Fn impl would be &'a i32, and this output has no relationship to the lifetime of the self borrow in the call().

The mutable case is different because mutable references are move-only, not Copy. As such, returning it from a mutable borrow of self needs to reborrow the mutable reference, and that needs a lifetime association between mutable borrow of self and the returned reference.

2 Likes

@vitalyd Thanks for your reply – makes all sense to me now. The important thing I was missing was indeed that immutable references are Copy.

See also this discussion on StackOverflow.

1 Like