The rationale is probably that copying only needs read-only access, so shared reference is sufficient, and closures always capture in the “weakest” for possible.
On second thought, the more important reason is the following:
With Copy
types, the type can still be accessed after the closure is created. So you code like
fn main() {
let mut n = 1;
(0..3).for_each(|_| {
println!("{}", n * 10); // access by-value, copies `n`
n += 1;
});
println!("now n is {n}");
}
to work as expected, printing
10
20
30
now n is 4
whereas using a move
closure will change the behavior to
10
20
30
now n is 1
(which IMO in and by itself is somewhat questionable too that it does this without any warning, but most definitely should not become the default behavior for non-move
closures, too)
Yeah, there’s certainly an upper bound for cost of using a reference, whereas Copy
types can be arbitrarily expensive.
The borrow checker is just that, a checker. It deliberately never influences behavior. This means that projects like rust-gcc
can get away with not having a borrow checker whilst still being useful. More importantly, this means that the user doesn’t need to be able to do a full borrow-checking pass in their head in order to determine which way their closures capture their variables. Also: the borrow checker can get smarter over time without such improvements leading to breaking changes. I.e. the question
Q: are there problematic lifetime requirements in this code that could be solved by making this closure move
its capture
– in particular the first part “are there problematic lifetime requirements in this code” – could be answered with “yes” today, and with “no” in the future for code that only newer borrow checking algorithms, e.g. the things that polonius does, can accept. This means that any rule like
If question Q above is answered “yes” then change the closure capture behavior
can result in behavioral changes, and thus breakage: maybe not the most realistic scenario, especially for a Copy
type, but…: e.g. capturing by-value in a particular case could’ve not only solved lifetime issues but also solved a closure fulfilling Send
whilst capturing a non-Sync
type. If e.g. polonius could solve the lifetime issue, and the behavior changed to capture-by-reference, then the Send
bound would be broken. Editions also wouldn’t help, unless we want to keep old versions of the borrow checker around indefinitely (and we definitely do not want that), in order to ensure unchanged behavior in older editions.