Skip to content

Moving structs with Copy fields into closures causes surprising behavior #111376

Open
@alexblanck

Description

@alexblanck

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 }

Playground Link

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

cuviper commented on May 10, 2023

@cuviper
Member

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, and move makes that a copy just for that u64.

alexblanck

alexblanck commented on May 15, 2023

@alexblanck
Author

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.

added
A-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.
A-closuresArea: Closures (`|…| { … }`)
T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
and removed on Sep 7, 2023
fmease

fmease commented on Sep 7, 2023

@fmease
Member

Related to (but still distinct from) #73467.

added
A-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.
and removed
A-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.
on Dec 21, 2024
Walter-Reactor

Walter-Reactor commented on Apr 10, 2025

@Walter-Reactor

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

monoid commented on May 1, 2025

@monoid

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-closuresArea: Closures (`|…| { … }`)A-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.C-bugCategory: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @cuviper@monoid@alexblanck@ChrisDenton@fmease

        Issue actions

          Moving structs with Copy fields into closures causes surprising behavior · Issue #111376 · rust-lang/rust