Why does moving a disjoint field capture into a closure differ when the type is a value vs a reference?

Viewed 83

As explained in Why is the move keyword needed when returning a closure which captures a Copy type? and How to copy instead of borrow an i64 into a closure in Rust?, if a closure captures a type that implements Copy, we need to use the move keyword to eagerly copy the value:

fn use_move_to_copy_into_closure() -> impl FnOnce(i32) -> bool {
    let captured = 0;
    move |value| value > captured
}

As of Rust 2021, disjoint capture in closures means that only the specific fields used in a closure are captured by the closure:

struct Wrapper(i32);

fn edition_2021_only_captures_specific_fields(captured: Wrapper) -> impl FnOnce(i32) -> bool {
    let ret = move |value| value > captured.0;
    drop(captured); // fails in 2015, 2018, succeeds in 2021
    ret
}

If I capture a Copy field belonging to a reference, however, the field is not copied:

struct Wrapper(i32);

fn capturing_a_field_of_a_reference(captured: &Wrapper) -> impl FnOnce(i32) -> bool {
    move |value| value > captured.0 
}
error[E0700]: hidden type for `impl Trait` captures lifetime that does not appear in bounds
  --> src/lib.rs:15:60
   |
15 | fn capturing_a_field_of_a_reference(captured: &Wrapper) -> impl FnOnce(i32) -> bool {
   |                                               --------     ^^^^^^^^^^^^^^^^^^^^^^^^
   |                                               |
   |                                               hidden type `[closure@src/lib.rs:16:5: 16:36]` captures the anonymous lifetime defined here
   |
help: to declare that the `impl Trait` captures `'_`, you can add an explicit `'_` lifetime bound
   |
15 | fn capturing_a_field_of_a_reference(captured: &Wrapper) -> impl FnOnce(i32) -> bool + '_ {
   |                                                                                     ++++

I expected that the field .0 would be copied in, just like in the unwrapped i32 or the owned Wrapper cases. What causes this difference?

3 Answers

move captures the environment by value, but things in the environment that are references remain references -- the references are captured by value.

Put another way, move closures only try to move values, because otherwise there wouldn't be a way to simultaneously capture some things by value and some things by reference. For example, it's a common pattern to do this when dealing with threads in e.g. crossbeam. Assume these structs:

#[derive(Clone)]
struct Foo;

impl Foo {
    pub fn baz(&mut self, _: i32) {}
}

struct Bar(pub i32);

And this snippet:

let foo = Foo;
let bar = Bar(0);

{
    let mut foo = foo.clone();
    let bar = &bar;
    s.spawn(move |_| {
        foo.baz(bar.0);
    });
}

Here the closure takes ownership of the clone of foo but references bar.0. This is true even if the type of bar.0 is Copy.

If it didn't work this way, there would be no way to express that the closure should own the foo clone, but borrow the copyable value bar.0.

Remember that implementing Copy on a type only means that an attempt to move a value of that type will instead copy. Since captured is a reference in your third example, capturing captured.0 doesn't attempt to move the i32 like it does in your second example where you have an owned value, and if a move isn't attempted then no copy can happen.

First of all, it should be acknowledged that your code errors in both edition 2018 and 2021, so to rephrase your question, it would be: why doesn't capture_disjoint_fields/RFC 2229 allow the field to be copied into the closure?

The RFC has a confusing paragraph:

When generating a capture expression, we must decide if the output should be owned or if it can be a reference. In a non-move closure, a capture expression will only produce owned data if ownership of that data is required by the body of the closure. A move closure will always produce owned data unless the captured binding does not have ownership.

So perhaps it does not move/copy data from your wrapper type because "the captured binding does not have ownership"? This might not be the exact reason since documentation around closures are specifically vague about capturing.

The MIR has a small difference for this snippet: (play)

struct Wrapper(i32);

fn ref_non_move(captured: &Wrapper) -> impl FnOnce(i32) -> bool + '_ {
    |value| value > captured.0
}

fn ref_move(captured: &Wrapper) -> impl FnOnce(i32) -> bool + '_ {
    move |value| value > captured.0
}
--- a/a.mir
+++ b/b.mir
@@ -1,6 +1,6 @@
-fn ref_move::{closure#0}(_1: [closure@src/lib.rs:8:5: 8:36], _2: i32) -> bool {
+fn ref_non_move::{closure#0}(_1: [closure@src/lib.rs:4:5: 4:31], _2: i32) -> bool {
     debug value => _2;
-    debug captured => (_1.0: &Wrapper);
+    debug captured => (*(_1.0: &Wrapper));
     let mut _0: bool;
     let mut _3: i32;
     let mut _4: i32;
@@ -12,4 +12,3 @@ fn ref_move::{closure#0}(_1: [closure@src/lib.rs:8:5: 8:36], _2: i32) -> bool {
         return;
     }
 }

This difference could be significant, since ref_non_move did not work before 2021, but I'm not sure if it is actually capture_disjoint_fields or just a rework in the capture system.

The reference says:

If the move keyword is used, then all captures are by move or, for Copy types, by copy, regardless of whether a borrow would work. The move keyword is usually used to allow the closure to outlive the captured values, such as if the closure is being returned or used to spawn a new thread.

Which explains that &Wrapper is simply copied to the closure, but does not justify why it should be the case, which I think is what you are really looking for.

This part of the documentation should definitely be improved, and this question is one of the holes that needs to be fixed. Conclusion: still confused.

It's not a question of whether the copy occurs, but when it occurs. The copy only happens when the closure is called, not when it's defined (how could it be -- a captured hasn't been provided yet). Since the closure outlives the function call, it needs to know that its reference to captured will be valid when it's actually called precisely so that it has something to copy. Therefore it needs to be annotated with a lifetime tying it to the lifetime of captured. Thanks to lifetime inference, it's sufficient to simply add '_ to the impl instead of needing to explicitly write fn capturing_a_field_of_a_reference<'a>(captured: &'a Wrapper) -> impl 'a + FnOnce(i32) -> bool.

Related