Multiple mutable borrows error with if let

I want to get the following code working (it needs to be a static method, no self, and good judgement prevents me from using multiple get_mut calls or some other hack -- the map is large; using entry API unconditionally is a no-go due to unconditional clone):

    fn get<'slf>(
      map: &'slf mut HashMap<String, usize>,
      key: &str,
    ) -> &'slf mut usize {
      if let Some(data) = map.get_mut(key) {
        data
      } else {
        let data = 42usize;
        map
          .entry(key.to_string())
          .or_insert(data)
      }
    }

Here is the full playground.

error[E0499]: cannot borrow *map as mutable more than once at a time

It's not clear to me why Rust complains about multiple mutable borrows, when they should apply to different branches. As per my reading of rust 🚀 - &mut self borrow conflicting with itself. | bleepcoder.com this should have been fixed with non-lexical lifetimes (i.e., a couple years back...). Alas, it's still not working.

I am currently using a bunch of ugly unsafe to work around the problem and Miri does not complain. But that gets me back to the good judgement above...

Are there suggestions for a non-unsafe solution?

Here are two more variations of what I believe to be similar problems. with is like with2 but without the explicit lifetime (which I can elide there, but I cannot elide it in the get example which is really what I need). with compiles and works fine, suggesting to me that the named lifetimes is tripping Rust over. But I am unsure how that finding helps me, as I cannot craft anything like this for the original get code. I'd still be curious as to why exactly that makes such a difference -- named or not, the reference has to outlive the function call and must not exceed the lifetime of self.

There's a WIP version of the borrow checker called polonius which accepts your code. It's not a solution to use, but it's a good indicator to see that your judgement is correct about the code being sound.

2 Likes

I find the requirement of entry to unconditionally take an owned key somewhat annoying myself. Arguably it should also be okay to provide something that can be turned into a key on-demand, for example using the ToOwned trait.

If you use hashbrown::HashMap, you can work around these limitations with the raw_entry_mut method.

fn get<'slf>(map: &'slf mut HashMap<String, usize>, key: &str) -> &'slf mut usize {
    map.raw_entry_mut().from_key(key).or_insert_with(|| {
        let data = 42usize;
        (key.to_owned(), data)
    }).1
}
1 Like

Thanks for the pointer on how to quickly try things out under polonius!

Fair point regarding hashbrown's HashMap. I don't want to use a different crate just for this one reason -- in my opinion it should be possible to have an optimal hashmap element insertion with standard Rust and without having to reimplement entire collections from scratch. It appears as if that will be possible through the raw entry functionality as that is one of the "exotic situations" it caters to. Unfortunately, that doesn't help me on stable Rust at this point in time.

Something that works today is to use contains_key in the if and then do .get_mut(key).unwrap() in the true branch.

1 Like