I'm playing around with scoped threads (I learned about them from an earlier post I made here) and I stumbled upon something I don't understand. Here's the code:
let v = vec![String::from("foo"), String::from("bar")];
thread::scope(|s| {
for item in &v {
s.spawn(move || { // <---- Confusion!
println!("item = {}", item);
});
}
});
I have absolutely no idea why is the move necessary here. If I don't add the move, I get this error:
error[E0373]: closure may outlive the current function, but it borrows `item`, which is owned by the current function
My understanding is that, if I don't use move, then Rust will attempt to capture things by reference, and move makes it capture things by value (which will result in a move for non-Copy types). However, here, item is an &String with or without the move, so what do I win by moving it into the closure?
I'm guessing this is something pretty basic, but after looking around for an explanation I'm still very confused
You win the fact that the closure captures (using the notion of "lifetime name = value name") &'v String, i.e. the reference to the item allocated outside the scope, and not &'item &'v String, which is invalid after the closure passed to scope returns and item is dropped.
let v = vec![String::from("foo"), String::from("bar")];
thread::scope(|s| {
for item in &v {
s.spawn(|| {
println!("item = {}", *item); // or &*item
});
}
});
This can be useful if you’re also accessing other variables from outside the scope inside of the closure and want to avoid move affecting their usage.
The problem is that in this context, the reference item is used by-reference (which is implicitly created in the println!), so writing something like &*item solves the problem; in this case *item works, too, as print statements create implicit borrows anyways. A re-borrow (or dereference) at every use-site of item will result in the closure capture algorithm seeing that item is not needed in the closure but only *item is, so the closure captures (a borrow of) *item which lives sufficiently long, as it’s based on a borrow of v living outside of the scope.
TL;DR, if you want to capture a reference in a non-move closure not by-reference (i.e. reference to a reference), make sure to dereference it at every use-site.
This will happen actually implicitly (via re-borrows / or auto-deref) in many cases already, but not in all cases.
references of types are themselves types, so String is a type, &String is also a type. if you don't use the move keyword, the closure will capture item (which has the type &String) by reference, that is , it will capture a &&String inside the closure type.
That's not true in edition 2021, at least, not always. See also the previous comment for more detail, and some subsequent comments for more discussion of edition differences.
Ah, it took me a while to understand what you're saying, but I think I get it now.
So, essentially, without the move I'm passing a "reference to item" (which is, in turn, a reference itself). So, once the for loop ends, the "reference to item" will be dropped, and thus the compiler cannot accept passing that to the closure. Using move I am moving that reference itself into the closure, so that it won't be dropped by the for loop machinery.
No, it is item itself that is dropped at the end of the current iteration. Thus you are not allowed to have a reference to it. By "moving" (=copying) the item into the closure, the closure will have an independent copy of it.
Dropping a reference doesn't in itself do anything actively, in the sense that it doesn't invalidate other references to its referent. Dropping a value does invalidate any references to the value, though. (And it doesn't matter what type that value has; it's the same whether or not it's a reference.)
Ah, I think the part that had not clicked for me was that "moving" a reference is actually just performing a copy so that the closure has its own copy of it - and thus it doesn't matter if the "original" reference is dropped early -.