0

I have a struct which maps ids to indices and vice versa.

struct IdMapping<'a> {
    external_2_internal: HashMap<&'a str, usize>,
    internal_2_external: HashMap<usize, String>,
}

impl<'a> IdMapping<'a> {
    fn new() -> IdMapping<'a> {
        IdMapping {
            external_2_internal: HashMap::new(),
            internal_2_external: HashMap::new(),
        }
    }

    fn insert(&'a mut self, internal: usize, external: String) {
        self.internal_2_external.insert(internal, external);
        let mapped_external = self.internal_2_external.get(&internal).unwrap();
        self.external_2_internal.insert(mapped_external, internal);
    }
}

If I am using this structure the following way

fn map_ids<'a>(ids: Vec<String>) -> IdMapping<'a> {
    let mut mapping = IdMapping::new();

    for (i, id) in ids.iter().enumerate() {
        mapping.insert(i, id.clone());
    }

    mapping
}

I receive the following compiler error:

error[E0499]: cannot borrow `mapping` as mutable more than once at a time
  --> src/lib.rs:28:9
   |
24 | fn map_ids<'a>(ids: Vec<String>) -> IdMapping<'a> {
   |            -- lifetime `'a` defined here
...
28 |         mapping.insert(i, id.clone());
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `mapping` was mutably borrowed here in the previous iteration of the loop
...
31 |     mapping
   |     ------- returning this value requires that `mapping` is borrowed for `'a`

Playground link

Why can't I mutably borrow the mapping for its insert method each iteration of the loop? How should I implement this use case?

kmdreko
  • 42,554
  • 6
  • 57
  • 106
Janek
  • 3
  • 3
  • I am very sorry about that mistake. I tried to remove the references to my problem domain to make it more accesible. I have update the example – Janek Jul 11 '22 at 16:51
  • Thanks for updating! And its more for your benefit than mine since good questions will hopefully get you good, prompt answers. – kmdreko Jul 11 '22 at 16:55
  • Yes. Thank you for pointing it out! – Janek Jul 11 '22 at 17:59

1 Answers1

0

The problem is due to the self reference in your struct.

Let's first look at whether this is theoretically sound (assuming we're writing unsafe code):

  1. HashMap uses a flat array (quadratic probing), so objects in the HashMap aren't address stable under insertion of a new element. This means that an insertion into internal_2_external may move the existing Strings around in memory.
  2. String stores its content in a separate heap allocation, the heap allocation remains at the same location. So even if the String is moved around, a &str referencing it will remain pointing to valid memory.

So this would actually work if implemented in unsafe code.

While it's logically sound to do those operations, the type system is unable to recognise that you can move a String while keeping borrowed ranges of it valid. This means that if you borrow a &str from a String, the type system will prevent you from doing any mutation operation on your String, including moving it. As such, you can't do any mutation operation on the hash map either, giving rise to your error.

I can see two safe ways to work around this:

  • Make external_2_internal keep its own copy of the strings, i.e. useHashMap<String, usize>.
  • Keep the strings in a separate Vec:
struct IdMapping {
    strings: Vec<String>,
    external_2_internal: HashMap<usize /* index */, usize>,
    internal_2_external: HashMap<usize, usize /* index */>,
}

Alternatively you could work with unsafe code, if you don't want to change the layout of your struct.

Bernard
  • 5,209
  • 1
  • 34
  • 64
  • Thanks for your quick answer. I have a Java background where this pattern doesn't impose any problems and I was trying to wrap my head around this for a while now. I will try using the vector approach. – Janek Jul 11 '22 at 18:02