My expectation is that the closure func takes ownership of the struct instance x and that the program would not compile because there is an attempt to mutate x after its ownership was moved to the closure.
Instead the program compiles fine and provides the output
Found x.0 = 16
Can someone explain why this work?
struct MyType {
member: u32,
}
fn main() {
let mut x = MyType {member: 15};
let mut func = move || {
x.member +=1;
};
func();
x.member += 1;
// This prints
println!("Found x.0 = {}", x.member);
}
This is Rust trying to get smart and move only a single used field, not the whole struct (and since it's Copy, struct is not invalidated). You can see that if field is not Copy, for example String, this is an error:
I understand, but it seems very wrong to me. Any reasonable interpretation of this code (by a human) would be that x.member += 1 is meant to refer to the field in the struct. Instead it results in an unexpected bug.
Is this really considered correct and desirable? Or was it something accidental and now has to be supported for compatibility?
Yes, that's very nice, I agree. It's allowing it when the struct is referenced after calling the closure, and closure mutates the struct, that strikes me as dangerous.
I thought it was too pedantic to point this out before, but since it's being directly talked about now -- I'm not sure I'd call the specific example at hand a bug. The disconnect for the OP is that x wasn't moved, like it is on edition 2018 and before. But when that's the case, the program doesn't compile at all. Can a program that only newly compiles be bugged?
I can see the potential for a "16 vs 17" bug though. In that case the bug was putting move on the closure when you wanted to capture by &mut _. But it is also probably something pretty hard to correctly lint against: both the behavior with and without move on the closure are completely valid programs that do different things. Programmer intent is hard to lint, and the suggested change can result in different behavior, so false negatives would be bad.
The surprise (to some) of move not actually moving Copy values exists on all editions (although it's true that it's significantly less likely to hit when move captures the whole containing value, pre-2021). Maybe there could a be a restriction lint that just lets you know when you've copied something into a move closure.
Composite types such as structs, tuples, and enums are always captured entirely, not by individual fields. It may be necessary to borrow into a local variable in order to capture a single field:
The example given does not use move, but the statement above is general so it seems to include move captures.
If, instead, the closure were to use self.vec directly, then it would attempt to capture self by mutable reference. But since self.set is already borrowed to iterate over, the code would not compile.
does compile when self.vec is used directly.
I couldn't find an issue about the outdated doc for the reference. Should I file an issue?
I also didn't see anything mentioned about this feature in the book.
I think the lint I'd prefer to trigger here is unused_assignments, which would trigger in the case of top-level variables but doesn't currently for struct members. That would clue you in that the closure doesn't do anything.
let mut sum = MyStruct{ a: 1, b: 0};
(1..10).for_each(move |x| {
// This will not trigger a warning for unused variable
// as sum.b will be treated as a Non-tracked place
sum.b += x;