Suggestion -- unused move warning

#1

I was doing some experimenting with closures, trying to understand the behavior of move closures. I ran this little program in the Playground (the new web-site design has made this useful tool too hard to find, in my opinion):

fn main() {
    let foo = Box::new(42);
    let bar = move || {
        println!("{}", foo);
    };
    bar();
    bar();
}

and was surprised when it compiled without error and printed 42 twice. I thought ‘foo’ was moved into the closure and that therefore I couldn’t invoke the closure twice. After a bit of head-scratching, I realized that the println! macro was probably expanding into a reference to foo. That was confirmed by this:

fn main() {
    let foo = Box::new(42);
    let bar = move || {
        let baz = foo;
        println!("{}", baz);
    };
    bar();
    bar();
}

which produced what I’d expected earlier:

Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `bar`
  --> src/main.rs:10:5
   |
9  |     bar();
   |     --- value moved here
10 |     bar();
   |     ^^^ value used here after move
   |
note: closure cannot be invoked more than once because it moves the variable `foo` out of its environment
  --> src/main.rs:6:19
   |
6  |         let baz = foo;
   |                   ^^^

error: aborting due to previous error

My suggestion, if it is possible, is that the compiler warn about an unused move, just as it does with unused mut. I just checked the compiler documentation and could not find such a warning. Perhaps I missed it, but if it’s not already there and is possible to do …

1 Like

#2

There are uses for this. If you were returning that closure from a function, then it needs to take ownership of those local variables, even though it only uses a reference when called.

2 Likes

#3

But presumably the compiler could detect that case. What I am advocating is a warning when the ‘move’ keyword is superfluous and it’s not in the case you cite.

0 Likes

#4

I suspect it’s not trivial to detect in general, but maybe. It would have to be able to determine that the overall effects are identical with or without the move.

At a high level, this seems like it might be a better candidate for a clippy lint, although I think they have less information to work with in resolving such cases.

0 Likes

#5

Well, I’m speculating too, but my guess is that the compiler knows whether a move is actually occurring, since it has to generate the code to support it.

I’m not asking for the impossible. I’d just like the developers to consider this.

0 Likes

#6

It certainly knows whether a move is required in calling the closure, or what kind of references are needed, which decides whether it may be Fn, FnMut, or just FnOnce. If the closure consumes a non-Copy value, it must be FnOnce, and that’s indeed superfluous with the move keyword if there are no other references. I suppose the compiler could easily warn about that.

But your original case would have captured by reference, if not for the move. So there was a real effect here, where move gave you an Fn closure that still owned all of its values. If nothing else, this could have a small performance benefit, since the foo integer can be read with only the Box pointer indirection, rather than reference to Box to integer. I think it would be really hard for the compiler to decide whether such a move is “unused”.

1 Like

#7

Now you’ve gone and confused me, but you’ve brought up something essential I thought that in my original example that because no move was required by the closure, the ‘move’ keyword was simply being ignored. I thought the proof of that was that I could call ‘bar’ twice. If ‘foo’ had been moved, then it would have been dropped after the first call to the closure and so the second call would be invalid, which I would have expected to have elicited a slap on the wrist from the compiler. But if I do this:

fn main() {
    let foo = Box::new(42);
    let bar = move || {
        println!("{}", foo);
    };
    bar();
    bar();
    println!("{}", foo);
}

the compiler says

Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `foo`
 --> src/main.rs:9:20
  |
4 |     let bar = move || {
  |               ------- value moved into closure here
5 |         println!("{}", foo);
  |                        --- variable moved due to use in closure
...
9 |     println!("{}", foo);
  |                    ^^^ value borrowed here after move
  |
  = note: move occurs because `foo` has type `std::boxed::Box<i32>`, which does not implement the `Copy` trait

error: aborting due to previous error

whereas if I do this:

fn main() {
    let foo = Box::new(42);
    let bar = || {
        println!("{}", foo);
    };
    bar();
    bar();
    println!("{}", foo);
}

then the result is

Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/playground`
Standard Output
42
42
42

So clearly the ‘move’ keyword is making a difference.

I am now completely at a loss to understand what this thing is doing. If the original example is really moving, as the keyword says, then how can I call bar twice in that case? Which suggests that perhaps the compiler is optimizing away the move in the original case and not in the second, where it can’t? This is the kind of thing that pushes me to the brink of just abandoning use of this language because if is so freaking complicated that only an anointed few can understand it and it’s damned near impossible to document. It just feels like writing code in this language is one research project after another, instead of … writing code.

0 Likes

#8

It’s moving into the struct that represents the captured closure environment, but the function is still callable as Fn using just &self, which allows repeated calls.

@KrishnaSannasi wrote a detailed blog post on closures that may help:

4 Likes

#9
fn main() {
    let foo = Box::new(42);
    let bar = move || {
        println!("{}", foo);
    };
    ({bar})(); // This call does take `bar` by value
    bar(); //~ Error, use of moved value `bar`
}
  • Other example (this one mimics the currently unstable traits in stable Rust)
0 Likes

#10

@Yandros, you’re actually complicating this. :slight_smile:

Your example now reveals whether the closure is Copy or not – {bar} is a trick to force a move of a value, but the original still remains if its a copyable type. That is, bar() calls the Fn directly referencing &self, but ({bar})() moves the closure into a temporary, then calls that Fn.

If the closure only references the Box, it can be trivially copied. When you move the Box into the closure, it now owns that heap allocation and has to drop it, so it’s no longer a Copy type.

0 Likes

#11

I just wanted a fast example to show the difference between calling bar by value or by reference ; to further prove your point. Talking about the closure becoming automagically Copy may indeed be confusing, though, so i’ll edit that part out.

0 Likes

#12

You (cuviper) said: “It’s moving into the struct that represents the captured closure environment, but the function is still callable as Fn using just &self , which allows repeated calls.”

So it’s owned by the closure’s struct? But multiple calls to the closure are allowed? So when does the moved object get dropped?

0 Likes

#13

When bar (the closure) is dropped!

1 Like

#14

I recommend reading my blog post that @cuviper posted. It details how closures are desugared and will likely answer most if not all of your questions related to closures given that you know how normal structs work. I made that post for the express purpose of teaching people how closure work because there is a lot of confusion around them and they seem like magic, hence the title.

Thanks you @cuviper!

0 Likes

#15

I did read your post and it is essential reading if closures are to be used, providing the information that is missing from both the Rust book and Blandy and Orendorff about the various ways captured variables are stored, in what scope and under what circumstances the different methods are chosen. Without this, I was completely at a loss to understand the behaviors I was seeing from my little examples. Now I get it.

Thanks very much for doing this.

1 Like

#16

There are indeed plenty of blog posts about closures. Take, for instance, the following one from 2015!

0 Likes

#17

Also helpful. Thank you.

0 Likes