Basic question about closures, move, Vec<T>

I need a clarification about closures in Rust.

In section 3.23 of the "Official Rust Book", section "Closures and Their Environment", the author gives an example of how a closure takes ownership of a Vec in its environment. But I don't understand why this has to happen -- why can't the closure just contain an immutable reference to the Vec? The explanation in the book mentions that Vec is not copyable, but I don't see how that's relevant, since the 'move' keyword wasn't used. Did I miss something in the previous chapters?

(Overall, I enjoy the Rust book. It's not as detailed as I would like, but most things that weren't stated explicitly, I can figure out from previous chapters).

Relevant chapter is here: Closures

1 Like

Assuming you're talking about this example:

let nums = vec![1, 2, 3];
let takes_nums = || nums;
println!("{:?}", nums); // error

So usually, if there's a move keyword, the closure moves (or copies) the environment. Otherwise the closure borrows from the environment (ie. internally contains a reference). But there's a small exception from this rule – if from a closure's body it's obvious that it has to move a particular variable, this variable is moved instead of being borrowed.

So in this case, the closure returns a fully owned vec. It couldn't do it if it was only borrowed! Sidenote: it doesn't matter if the closure runs or not, but the issue is more obvious if it's run:

let nums = vec![1, 2, 3];
let return_nums = || nums;
let new_nums = return_nums();
println!("{:?}", nums); // error

If the closure was doing something with the vector, but not actually returning it (or using some function that takes self by move), it'd only borrow the vec:

let nums = vec![1, 2, 3];
let get_len = || nums.len();
let len = get_len();
println!("{:?}", nums); // fine!

PS, if you're stuck on some topic, you could try reading the upcoming second edition of The Book

1 Like

I must admit I always thought cases like this should require an explicit move. As it stands, why does the closure have to take ownership in this example? It could just as well return &Vec and things would work with less surprises.

Forget about the move keyword for a second.

Rust is clever enough to automatically figure out what kind of ownership over the environment your closure needs. For example, if you were to write:

let takes_ref = || nums[0];
println!("{:?}, {}", nums, takes_ref());

Then takes_ref would only capture &nums, an immutable reference to the vector, because that's enough to access its first element. (If you care about low-level details about why that is true, v[0] desugars to v.index(0), which, due to autoref/autoderef, is the same as (&v).index(0). This is why you only need an immutable reference.)

If you were to modify the vector inside the closure, like so:

let takes_mut_ref = || nums.push(4);

Rust would figure out you need to capture a &mut reference to the vector. If you were to do something that requires moving the vector, like this:

let moves_it = || nums.into_iter().sum();

Then it would move the vector into the closure. (Of course, you could use iter() instead of into_iter() if you really meant to calculate the sum; I'm just using this as an example of a method that moves the vector.)

Now, in your example, you make the closure that returns the vector to the caller, so of course it has to move it.

No, it cannot return a &Vec; it's been ordered to return the complete Vec, this is why it has to move it. If you wanted it to return &Vec, write it like this:

let takes_ref = || &nums;

Then it will return a reference, and Rust will see that it only needs a reference and it will make it only capture a reference. If you're returning an object directly it has to move it.


Now, there is one last detail: the move keyword. If it is enough for your closure to capture a reference, but you nevertheless want to move the object, then, and only then, you use the move keyword -- it tells Rust to just move the thing instead of inferring what's necessary:

let v1 = vec![1, 2];
let takes_ref = || &v1;
print!("{:?}", v1);  // works

let v2 = vec![1, 2];
let moves = move || &v2;
print!("{:?}", v2);  // error

Why would you ever want that? Sometimes you need a closure that does not capture any references, for example, when sending a closure to another thread:

let v = vec![1, 2];
// won't compile:
thread::spawn(|| println!("{}", v[0]) );
// works:
thread::spawn(move || println!("{}", v[0]) );

It is enough for the closure to only capture a reference to v; it doesn't need the full v moved; but you couldn't send such a closure to another thread (because the main thread could then deallocate v before the closure gets a chance to dereference its reference).

So, to summarize, most of the time Rust will do the right thing automatically; and the move keyword is there for special cases when there's an additional constraint on the closure because of how you're planning to use it.

15 Likes

That's fair.

1 Like

Thanks! I had wondered about this too and this was an excellent explanation!

1 Like

Thanks! So the way I understand it now, when a closure attempts to capture an environmental variable, Rust can infer whether borrowing, copying, or moving is needed, based on 1) what is returned (name versus &name), 2) the type of what is returned (is name copyable or not?), 3) how the environmental variable is used within the body of the closure ( name[0] versus name.push(3) ), and 4) whether the programmer includes the 'move' keyword.

And those are the only factors that determine how the environmental variable is captured. The original binding in the environment plays no part, and the programmer usually does not explicitly specify the nature of the capture.

Is that a complete outline of the cases I need to consider when using a closure?

1 Like

Well, while what you say is technically correct, it sounds like you're still confused, so let me explain:

First of all, you should stop thinking about moving and copying as two different things. I mean, yeah, when you talk about "moving vs copying" (like in this section of the book), then those are two opposite things, but when it comes to "& vs &mut vs move/copy" they're essentially the same, the only difference being whether the original value stays accesible after the move (for Copy types) or it doesn't (for non-Copy types). In most cases, the language treats them the same -- they even share the same syntax (this was not always the case -- it was deliberately added into the language).

With that out of the way, let's look at the closures again. There are two factors:

  • Rust infers whether the closure needs to borrow an object immutably, borrow it mutably or move/copy it depending on what the closure does with it. This includes things like passing the object (or a reference to it) as an argument to some function, calling a method on it (because methods are just syntactic sugar for UFCS) or passing it to the future caller (i.e. returning it). This corresponds to your points 1)-3), but these are not separate rules! Rust looks at how the closure uses it and infers how to capture it, that's all. Oh and yeah, if it decides to move/copy it, then whether it'll be copied or moved (that is, whether the original value stays accessible) depends on its type, just like move/copy works in any every place -- but that's not specific to closures and captures.
  • It does all of that unless you specify the move keyword, which always forces move/copy, no matter how the closure uses the object.

The original binding does play a part, but not in a way you might expect. If, say, in the original binding obj is not an Object itself, but just an &Object reference, then what I described above applies to the obj, not the *obj. It can decide to borrow or move/copy obj, but not the *obj. That's okay, because you "cannot move out of borrowed content" anyway.

Yes, most of the time Rust does the right thing by default. The only reason to override it is when it is enough for the closure to take a reference (either mutable or immutable), but you don't want the closure to take any references (because you need it to be 'static), so you explicitly tell Rust to move/copy everything by using the move keyword -- but again, this is a rare case.

(In fact, there is a super-rare case where you want to force some of the environment to be moved/copied, but leave the rest borrowed -- and as far as I know, this is impossible to achieve in Rust. This need can arise, for example, with so-called scoped threads; I've cooked up an example here: Rust Playground)

5 Likes

That really helped. My brain's model of how this works is a lot better. Great follow-up.

1 Like

A simple yet beautiful solution proposed here: Force capture of a variable by value while capturing others by reference - #2 by vitalyd

Indeed, my example works with let outer = &outer; — now I only wonder how come I haven't thought of it myself.

Thanks a lot @vitalyd

To be perfectly frank, I recalled this thread but didn't recall the exact scenario you were demonstrating :slight_smile:. Anyhow, glad it works for your example too.