What's the purpose of the move keyword?

I thought the purpose was to make sure that the programmer realizes that values are being moved. The compiler always knows when it's missing, so it could infer it, but having the programmer put it in explicitly is a way of saying "hey, are you sure you meant to move these?". It also makes FnOnce closures more greppable.

But then I came across this example, where string is moved into closure, but the move keyword is not necessary:

let string = "string".to_string();
let closure = || drop(string);
closure();

So I guess my notion that move is there to flag FnOnce closures and make them opt in is misguided, since they can clearly exist without it? Or maybe the situations in which it can be left out are all very trivial, like the one above, so in practice, move still plays this role, even though in theory, it is not always obligatory?

1 Like

Closures have a default environment capture mode, which depends on what the closure does with each value that was captured by the environment.

Simplifying things a bit, if the value is only read, it is captured by shared reference. If it is written, it is captured by mutable reference. If it is moved away from the closure, it is moved in.

This is perfect for single-threaded code where the closure never escapes the scope which it originated from. But in multi-threaded code, and when closures are moved into containers, one often wants 'static data, which in the case of closure generally means avoiding references and moving everything into the closure. The move keyword allows one to do that.

These are the basic uses of these two closure operating modes. In practice, you can still "move references in" with a suitable let binding before the closure, so the "move" mode effectively allows you to fully control how the closure capture the environment and support any custom capture scenario.

5 Likes

You may also need move if you're trying to return something with a closure. That closure might only need references to some captured values, but move will make it take ownership anyway so you can leave the current stack frame.

4 Likes

You may also want to have an FnOnce closure that's 'static. For instance, you may be drop()'ing a String in there, which alone makes the closure FnOnce, but you may also do something else inside of it with some captured reference; to force the closure to take ownership over the value (rather than take a reference), you can make it FnOnce + 'static. This might be because you want to call this closure later on, when the current stack frame is not available (for example). So the two aspects are somewhat orthogonal.

3 Likes

Thank you very much for the replies, it's all much clearer now :slight_smile: So a closure becomes FnOnce when it takes ownership of at least one value from the surrounding scope, but that doesn't mean it automatically moves all of the values it captures, it can still reference some other values by shared or mutable reference. This won't do if I want to call the closure at a later point, when the stack frame in which it was defined (and where the values it references originally came from) is gone. That's when I need move.

1 Like

It becomes FnOnce when it moves something out of itself. Since it moves out of itself, it’s only callable once.

In the drop() example, the closure automatically moved the String into itself. In theory, the closure could’ve captured a reference to the String but then drop(&String) is a noop, which wouldn’t make sense and isn’t what’s intended.

1 Like

Just to be anal with terminology; keep in mind that all closures impl FnOnce.

What happens is that, without move, each thing used in the closure is borrowed, mutably borrowed, or moved in according to how that individual thing is used. Then the compiler implements as many of the Fn traits as it can. If a closure moves at least one thing out of its environment needs to move something out of its captured fields when it is called, then it is no longer capable of implementing FnMut or Fn.

move simply forces all bindings to move.

let x = vec![3];
let y = vec![3];
let _ = (|| (x.len(), y))(); // borrow x, move y
let z = x;  // ok; x was not moved

let x = vec![3];
let y = vec![3];
let _ = (move || (x.len(), y))(); // move x, move y
let z = x;  // ERROR: x was moved

Generally speaking, I do not believe move actually changes the set of traits that are implemented. For completeness, here's an example of a move Fn closure:

let vec = vec![3];
let func = move || { &vec; };

let borrow = &func;
borrow();
borrow(); // ok; it impls Fn

let x = vec; // NOT OK; vec was moved
2 Likes