Reputation: 109
So I have a function which looks like this
fn foo() {
let items = vec![0.2, 1.5, 0.22, 0.8, 0.7, 2.1];
let mut groups: HashMap<u32, String> = HashMap::new();
let mut group = |idx: f32| -> &mut String {
let rounded = (idx / 0.2).floor() as u32;
groups
.entry(rounded)
.or_insert_with(|| format!("{}:", rounded))
};
for item in items.iter() {
group(*item).push_str(&format!(" {}", item))
}
}
and this code does not compile, with the following error:
error: captured variable cannot escape `FnMut` closure body
--> src/main.rs:9:9
|
5 | let mut groups: HashMap<u32, String> = HashMap::new();
| ---------- variable defined here
6 |
7 | let mut group = |idx: f32| -> &mut String {
| - inferred to be a `FnMut` closure
8 | let rounded = (idx / 0.2).floor() as u32;
9 | groups
| ^-----
| |
| _________variable captured here
| |
10 | | .entry(rounded)
11 | | .or_insert_with(|| format!("{}:", rounded))
| |_______________________________________________________^ returns a reference to a captured variable which escapes the closure body
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
Edit
As @Sven Marnach pointed out, the problem here is that I could create 2 mutable references to the same object:
fn foo() {
// ...
let ok = groups(0.1);
let problem = groups(0.1);
}
Original (incorrect)
I think that Rust is telling me, the closure
group
is mutably capturing the variablegroups
and then returning a reference to an object owned bygroups
. So the danger here is that the following code would return a dangling pointer (sincegroups
is dropped when it goes out of scope afterfoo
finishes).fn foo() -> &String { /* ... */ return groups(0.1); }
So is there any way to return a reference from a captured mutable HashMap
like this?
Upvotes: 3
Views: 604
Reputation: 42197
I think that Rust is telling me, the closure group is mutably capturing the variable groups and then returning a reference to an object owned by groups. So the danger here is that if I were to write:
then I would be returning a dangling pointer (since groups is dropped when it goes out of scope after foo finishes).
No. If that were the case Rust could (and would) warn about that.
The problem is that the lifetimes around function traits are problematic, because they don't have a way to match the lifetime of the result to the lifetime of the function itself.
As a result, rust blanket bans returning any reference to captured data from a closure.
As far as I can tell the solutions are:
don't use a closure, instead pass in groups
as an argument into the function (anonymous or named)
use some sort of shared ownership and interior mutability e.g. have the map store and return Rc<RefCell<String>>
desugar the closure to a structure with a method, that way the lifetimes become manageable:
use std::collections::HashMap;
struct F<'a>(&'a mut HashMap<u32, String>);
impl F<'_> {
fn call(&mut self, idx: f32) -> &mut String {
let rounded = (idx / 0.2).floor() as u32;
self.0
.entry(rounded)
.or_insert_with(|| format!("{}:", rounded))
}
}
pub fn foo() {
let items = vec![0.2, 1.5, 0.22, 0.8, 0.7, 2.1];
let mut groups: HashMap<u32, String> = HashMap::new();
let mut group = F(&mut groups);
for item in items.iter() {
group.call(*item).push_str(&format!(" {}", item))
}
}
Note that the above stores a reference to match the original closure, but in reality I see no reason not to move the hashmap into the wrapper entirely (and the struct can init itself fully without the need for a two-step initialisation as well):
use std::collections::HashMap;
struct F(HashMap<u32, String>);
impl F {
fn new() -> Self { Self(HashMap::new()) }
fn call(&mut self, idx: f32) -> &mut String {
let rounded = (idx / 0.2).floor() as u32;
self.0
.entry(rounded)
.or_insert_with(|| format!("{}:", rounded))
}
}
pub fn foo() {
let items = vec![0.2, 1.5, 0.22, 0.8, 0.7, 2.1];
let mut group = F::new();
for item in items.iter() {
group.call(*item).push_str(&format!(" {}", item))
}
}
Upvotes: 4