Description
I noticed some counterintuitive behavior related to partial moves of Copy struct fields (ex: u64 fields) into closures.
Minimal Reproducible Example
#[derive(Debug)]
struct Statistics {
calls: u64,
successes: u64,
}
fn main() {
let mut statistics = Statistics {
calls: 0,
successes: 0,
};
let mut generator = move || {
statistics.calls += 1;
};
generator();
statistics.successes += 1;
println!("{statistics:?}");
}
This code compiles and prints
Statistics { calls: 0, successes: 1 }
What I Expected
I would have expected a borrow of partially moved value: 'statistics'
error at the println!
, since the closure takes ownership of the statistics.calls
field and then the field is later read by the print call.
The actual behavior, where the updates to statistics.calls
have no observable effect, looks just like a silent copy of the whole struct. This surprised me. When I wrote the code, I expected either a compiler error or an output of Statistics { calls: 1, successes: 1 }
.
Investigation
I experimented a bit with the types of the fields within the struct, and I do get the compiler error I expect if Statistics.calls
is a non-Copy type like Vec
.
I didn't know about partial moves when I wrote the initial code. I expected the whole struct to move into the closure. It's still surprising to me that partial moves can create this type of "silent copy" behavior depending on the type of the struct's fields.
I did find a similar issue titled Move closure copies :Copy variables silently, but the code that I wrote is not covered by the "unused variable" warning added in response to that issue.
Solution
I'm new to Rust so I'm not sure this is a bug, but it's still confusing behavior. Perhaps, like the other issue I linked, there is room for a lint or warning here. Something that warned me that the closure's modification of statistics.calls
had no effect would be helpful, for example.
Activity
cuviper commentedon May 10, 2023
This is a disjoint capture specific to 2021 edition. If you change your playground to 2018, you do get an error, though not exactly what you expected. The closure is only capturing the
calls
field, andmove
makes that a copy just for thatu64
.alexblanck commentedon May 15, 2023
Thanks for the link about disjoint captures. That's roughly what I gathered was happening, but I didn't know that term.
I still feel like a warning saying that the change to the captured, copied field had no effect could be helpful in this case.
fmease commentedon Sep 7, 2023
Related to (but still distinct from) #73467.
Walter-Reactor commentedon Apr 10, 2025
I just wound up hitting this too: https://users.rust-lang.org/t/unexpected-failure-to-capture-full-struct-with-move/128125
IMO, after reviewing the RFC I understand the motivation, but agree that a warning would be in order if either 1) the struct implements the Drop trait, or 2) when the captured variable is used after it's ostensibly been 'moved' into .
At minimum, the disjoint captures behavior should be documented in https://doc.rust-lang.org/book/ch13-01-closures.html
monoid commentedon May 1, 2025
Similar case: https://towardsdev.com/the-case-of-the-missing-metrics-a-rust-closure-mystery-e8e51c8ab484
BTW, the
cargo fix --edition
doesn't change the minimal example at the above link.