I've this simple MWE which isn't outputting what I expect:
use futures::{StreamExt, stream::iter};
#[tokio::main]
async fn main() {
#[derive(Debug)]
struct TrivialStats {
count: i32,
}
let mut stats = TrivialStats { count: 0 };
// let mut stats = Box::new(stats); // Adding this line makes it not compile (as expected)
// fn check_copy<T: Copy>(_t: T) {}
// check_copy(stats); // Does not compile because TrivialStats is not Copy
iter(0..100)
.map(|_item| {
async move {
stats.count += 1;
}
})
.buffer_unordered(10)
.collect::<Vec<_>>()
.await;
println!("Final stats: {:?}", stats);
}
It prints: ... { count: 0 } but I would expect it to print 100 or error that I'm not handling the mut refs properly. The compiler does provide a warning that:
value captured by `stats.count` is never read
did you mean to capture by reference instead?
However, changing stats to be Box<TrivialStats> makes the compiler error saying it can't copy:
main.rs(10, 21): move occurs because `stats` has type `Box<TrivialStats>`, which does not implement the `Copy` trait
However, TrivialStats is also not copy as can be verified by un-commenting the test above. Am I missing something obvious?
Box isn’t a reference — it’s ownership just with an indirection. So, adding Box does not change the situation — in both cases, a copy of count would be moved into the future. The way to force the async block to capture a reference is to explicitly take a reference and capture the reference:
However, this will not make your program compile, for the same fundamental reason that it would not compile without the move keyword: you're trying to create several concurrent futures that share the same counter, so they cannot all have a plain exclusive &mut borrow of the counter. In order to do that, you need the counter to be interior mutable, such as Cell or (for multiple threads) AtomicI32, along with a shared reference to the counter:
use futures::{StreamExt, stream::iter};
use std::cell::Cell;
#[tokio::main]
async fn main() {
#[derive(Debug)]
struct TrivialStats {
count: Cell<i32>,
}
let stats = TrivialStats { count: Cell::new(0) };
let stats_ref = &stats;
iter(0..100)
.map(|_item| {
async move {
stats_ref.count.update(|c| c + 1);
}
})
.buffer_unordered(10)
.collect::<Vec<_>>()
.await;
println!("Final stats: {:?}", stats);
}
Yes that makes sense; however, I am suprised that the original program compiles and gives a wrong output that expected. I do understand how to fix it, but felt the difference in the compile-ability based on the type of stats somewhat confusing. Specifically, I don't understand why it "copies" the stats in the original program (while it can't do so with the Box)
"stats has type Box<TrivialStats>, which does not implement the Copy trait"
which is saying it needs to copy it ( to capture it) but it cannot because it doesn't have the copy trait. Does it need to explained why Box cannot be copied?
Apologies for the confusion; allow me to clarify. My issue is that the original code (without Box) does compile, and outputs 0, while a naive read would expect it to output 100. I'd thought the compiler would point out the issue, but it compiles and prints 0 which is confusing.
Hmm, well it makes a copy of stats, but that is a bit useless, hence the warning about stats.count never being read. The thing that is printed is the original stats not the updated copy.
It doesn’t copy stats, it copies stats.count. Closures always capture the least they can, which is usually desirable to avoid borrow conflicts, but has this unfortunate surprise for things that are intended to be mutated.