Hi,
I have a HashMap<usize, Foo> that I want to iterate over in an arbitrary order that is set independently from the HashMap internal state. For context, this is for modelling, so I need the iteration order to be reproducible (for testing) and to be able to shuffle it with an Rng to avoid order effects when iterating over my Foos. I therefore cannot just use HashMap::values() and HashMap::values_mut() whose iteration order is not guaranteed nor can be shuffled.
For convenience, I was hoping to be able to bundle the HashMap together with a Vec<usize> that determines the iteration order. Implementing iter() following that vector is trivial, but iter_mut() is a different beast...
Here's what I attempted, and am not sure at all is sound:
use std::{collections::HashMap, mem};
use crate::foo::Foo;
mod foo {
use std::sync::atomic::{AtomicUsize, Ordering};
static ID: AtomicUsize = AtomicUsize::new(0);
pub struct Foo(usize);
impl Foo {
/// Ensure each `Foo` has its own unique index
pub fn new() -> Foo {
Foo(ID.fetch_add(1, Ordering::Relaxed))
}
pub fn id(&self) -> usize {
self.0
}
}
}
/// Mutable iterator over the collection of `Foo`s
struct BarIterMut<'a> {
index: usize,
bar: &'a mut Bar,
}
impl<'a> Iterator for BarIterMut<'a> {
type Item = &'a mut Foo;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.bar.iter_ids.len() {
let id = self.bar.iter_ids[self.index];
self.index += 1;
let foo = self.bar.foos.get_mut(&id).unwrap();
let foo = unsafe {
// SAFETY: we're holding a &'a mut Foo, and iter_ids
// does not have any duplicate by construction.
// Therefore, all "foo"s here are disjoint and live
// at least for 'a.
mem::transmute(foo)
};
Some(foo)
} else {
None
}
}
}
/// The collection of `Foo`s
#[derive(Default)]
pub struct Bar {
/// The vector dictating the iteration order
iter_ids: Vec<usize>,
/// The map itself
foos: HashMap<usize, Foo>,
}
impl Bar {
pub fn insert(&mut self, foo: Foo) {
let id = foo.id();
self.iter_ids.push(id);
self.foos.insert(id, foo);
}
pub fn foos(&self) -> impl Iterator<Item = &Foo> {
self.iter_ids.iter().map(|&id| self.foos.get(&id).unwrap())
}
pub fn foos_mut(&mut self) -> impl Iterator<Item = &mut Foo> {
BarIterMut {
index: 0,
bar: self,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn iter_mut() {
let mut bar = Bar::default();
let nfoos = 100;
for _ in 0..nfoos {
bar.insert(Foo::new());
}
// for miri to catch potential UB if an &mut is somehow duplicated
let mut_refs: Vec<_> = bar.foos_mut().enumerate().collect();
assert_eq!(mut_refs.len(), nfoos);
// check the iteration happens in the expected order
for (i, foo) in mut_refs {
assert_eq!(foo.id(), i);
}
}
}
Transmuting the &mut to an &'a mut feels dirty and I'm not 100% sure the reasoning in the safety comment is correct.
Running the test through miri yields the following error:
running 1 test
test test::iter_mut ... error: Undefined Behavior: trying to retag from <252046> for Unique permission at alloc63906[0x428], but that tag does not exist in the borrow stack for this location
--> /home/adrien/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/into_iter.rs:269:23
|
269 | Some(unsafe { ptr.read() })
| ^^^^^^^^^^
| |
| this error occurs as part of retag at alloc63906[0x428..0x430]
| while retagging field .1
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <252046> was created by a Unique retag at offsets [0x428..0x430]
--> src/lib.rs:86:32
|
86 | let mut_refs: Vec<_> = bar.foos_mut().enumerate().collect();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: <252046> was later invalidated at offsets [0x420..0x430] by a SharedReadOnly retag
--> src/lib.rs:35:23
|
35 | let foo = self.bar.foos.get_mut(&id).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this is on thread `test::iter_mut`
= note: stack backtrace:
0: <std::vec::IntoIter<(usize, &mut foo::Foo)> as std::iter::Iterator>::next
at /home/adrien/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/into_iter.rs:269:23: 269:33
1: test::iter_mut
at src/lib.rs:88:25: 88:33
2: test::iter_mut::{closure#0}
at src/lib.rs:80:18: 80:18
I'm tempted to trust miri over myself on this, but this leaves me with the following questions:
- If my code is actually sound, what's tripping miri here?
- If miri is indeed correct that there is an UB, what's wrong with my reasoning?
- Is there a way to achieve what I want besides keeping
iter_idsseparate from the hashmap and loop overiter_idsdirectly and useget_mutto obtain my&mut Foos? There is nothing wrong/blocking with that for my use case, it's just a little clunky and unergonomic (it also makes it slightly harder to keep consistency between the content of the map and the content of theVecas the set ofFoos changes...).
Thanks!