rustlifetime-scoping

For loop - Struct with lifetime 'a cannot borrow as mutable because it is also borrowed as immutable


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?


Solution

  • 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:

    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.