Why does this compile
#[derive(Debug)]
struct A; // non-copy non-clone
fn foo(mut xx: [A; 2]) -> impl FnOnce() -> () {
move || { dbg!(&mut xx); } // xx borrowed here
} // xx dropped here
^____ xx dropped while borrowed
Why does this compile
#[derive(Debug)]
struct A; // non-copy non-clone
fn foo(mut xx: [A; 2]) -> impl FnOnce() -> () {
move || { dbg!(&mut xx); } // xx borrowed here
} // xx dropped here
^____ xx dropped while borrowed
Since it's a move
closure, xx
is moved inside it and borrowed only during its call. Note that this code doesn't compile:
#[derive(Debug)]
struct A;
fn foo(mut xx: [A; 2]) -> impl FnOnce() -> () {
let closure = move || {
dbg!(&mut xx);
};
dbg!(&mut xx); // use of moved value
closure
}
Neither does this:
#[derive(Debug)]
struct A;
fn foo(mut xx: [A; 2]) -> impl FnOnce() -> () {
|| {
dbg!(&mut xx); // closure may outlive borrowed value
}
}
So in the context of move ||
closure, the &xx
in the body is two operations. Copy/move xx
then apply the &
and take a reference? This doesn't happen outside a closure? Just so I'm aware in the future, taking references with &
of a variable is not going to copy the variable in no other context?
No, the moving happens when the closure is constructed, the meaning of &xx
itself does not change at all.
Put simply, a closure is a struct with fields for the captured data.
A closure
let closure = || {
dbg!(&mut xx);
};
// and here's a call demo
closure();
will
struct TheClosure<'a> { xx_ref: &'a mut [A; 2] }
impl TheClosure<'_> {
fn call(self) -> () {
dbg!(&mut *self.xx_ref);
}
}
let closure = TheClosure { xx_ref: &mut xx };
call
method// and here's a call demo
closure.call();
Now, the move
keyword influences the type of the field in this struct. It’s not longer a (mutable or immutable) reference, but instead always an owned value of the same type as the variable being captured.
(By the way, I’m being deliberately a bit vague on the whole closure desugaring, because the whole story is quite complex with quite a few corner cases, especially if the finer-grained closure captures we got in edition 2021 are also taken into account. But it’s mostly a bunch of little convenience rules to make your life easier, nothing you actually need to learn in detail to understand the general principles.)
(I have been and will be also ignoring anything beyond FnOnce
for simplicity of the desugaring. Otherwise, we’d have a second kind of code analysis to discuss that’s being done to the closure body, and up to 3 different call
methods would be generated for the same closure, and the desugaring of calling the closure would need to choose the right call method to call.)
So with this keyword the thing changes as follows.
The move
closure
let closure = move || {
dbg!(&mut xx);
};
// and here's a call demo
closure();
will
xx
struct TheMoveClosure { xx_owned: [A; 2] }
impl TheMoveClosure {
fn call(self) -> () {
dbg!(&mut self.xx_ref);
}
}
let closure = TheMoveClosure { xx_owned: xx };
call
method// and here's a call demo
closure.call();
For determining these desugaring, the general idea is that a closure move || … code mentioning variable `foo` …
will always capture foo
by-value, whereas a closure || … code mentioning variable `foo` …
will inspect how foo
is actually used and downgrade the capture to by-mutable-reference or by-shared-reference if that’s all that’s needed. Well, and… with finer-grained capturing of struct fields etc. in edition 2021, the full story is a bit more complex, because it’s no longer always the whole variable being captured.
So it’s not &xx
or &mut xx
that changes meaning, it’s just that move
will make the closure analysis simpler and just see something like &xx
as “some code that uses xx
”, and no longer as “some code that uses xx
only in ways where access by shared reference would be sufficient”.