Mutable values across thread: Why do I not get warnings about use after move in this example (rustc vs cargo?)

I have a question about this example that compiles without errors. It's confusing because intuitively, I'd expect there either to be a warning about a use after move when I trying to modify it in the second thread.

It seems like copies of cat.age is made implicitly.

On rust playground and locally, it compiles and outputs fine,

But on godbolt seems to correctly (at least to me) fail to compile with appropriate use after move errors. Compiler Explorer

use std::thread;

struct Cat {
    name: String,
    age: u32,
}

fn main() {
    let mut cat = Cat {
        name: "whiskers".to_string(),
        age: 3,
    };

    let thread_1 = thread::spawn(move || {
        cat.age += 1;
        println!("The cat is {:?} years old in Thread 1", cat.age);
        // not returning the cat here, but if I do i will get the desired compile error of move after use
    });

    thread_1.join().unwrap();

    let thread_2 = thread::spawn(move || {
        cat.age -= 1;
        println!("The cat is {:?} years old in Thread 2", cat.age);
    });

    thread_2.join().unwrap();
    println!("The cat is {:?} years old after threads", cat.age);
}

In this example the output is

The cat is 4 years old in Thread 1
The cat is 2 years old in Thread 2
The cat is 3 years old after threads

After realising that godbolt compiles with rustc, I tried to compile it locally with rustc as well, and to my surprise I get the appropriate errors this time. But not cargo. Any reason why this behaviour is different?

❯ rustc src/main.rs

error[E0382]: use of moved value: `cat`
  --> src/main.rs:22:34
   |
9  |     let mut cat = Cat {
   |         ------- move occurs because `cat` has type `Cat`, which does not implement the `Copy` trait
...
14 |     let thread_1 = thread::spawn(move || {
   |                                  ------- value moved into closure here
15 |         cat.age += 1;
   |         ------- variable moved due to use in closure
...
22 |     let thread_2 = thread::spawn(move || {
   |                                  ^^^^^^^ value used here after move
...

error[E0382]: borrow of moved value: `cat`
  --> src/main.rs:28:57
...
For more information about this error, try `rustc --explain E0382`.

vs

❯ cargo build
warning: field `name` is never read
 --> src/main.rs:4:5
  |
3 | struct Cat {
  |        --- field in this struct
4 |     name: String,
  |     ^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: `cat_age` (bin "cat_age") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s

It's a change in Edition 2021, and rustc invoke Edition 2015 by default.

And partially borrow a Copy field is a common source of confusion. I think I have see numbers of same question on URLO, and a couple github issue on this problem. We really should fire a compiler warning somewhere.

3 Likes

Incidentally, you can set the edition under Overrides on Godbolt (or write in the flag).

1 Like

Thanks for the direction! So essentially in Edition 2021, because we aren't returning cat, the copy of the partially captured Copy field is dropped. Theres no data dependency with thread_2 as both thread have their own copies of age and it doesn't even trip the borrow checker.