Help with understanding the borrow checker

I have the following code snippet:

  fn does_not_work(t: &mut HashMap<String, i32>) -> &i32 {    
     let k = String::from("key"); 
     match t.get(&k)
         Some(v) => &v,    
         None => {   
             let v = 1; 
             t.insert(k, v);    
             &v     
         }    
     }    
  }  

It gives an error when trying to call t.insert: " cannot borrow *t as mutable because it is also borrowed as immutable". I assume this is because the result of the call to get is still in scope, and the borrow checker does not know enough to differentiate the Some case, which has a reference to data inside the table, from the None case. Is that correct?

If I then change the code to return a copy rather than a reference:

  pub fn works(t: &mut HashMap<String, i32>) -> i32 {
      let k = String::from("key");
      match t.get(&k) {
          Some(v) => *v,
          None => {
              let v = 1;
              t.insert(k.clone(), v);
              v
          }
      }
  }

This compiles without errors. Why is that? I'm still calling insert while holding onto the option.

This is a well known problem in the compiler where it rejects code that it should accept. The problem is that if you do something that borrows a variable (here t) and then returns the resulting borrow (here v) from the function, then it marks the variable (here t) as borrowed for the rest of the function in all possible execution paths from there, not just the ones where it gets returned.

Note: Returning the local v variable in your None branch could never work. You would have to call get again into the HashMap to get a reference that actually points inside the HashMap rather than one that points to the local variable, which is destroyed when you return. But once you fix that, you run into what I described above.

The version that works works because you don't return any references.

1 Like

Got it, thanks. This specific case aside, is there a correct workaround for the general problem illustrated here?

This looks like it is doing the same as the following using the entry API:

t.entry(k).or_insert(1)

This returns an &mut reference to the value already in the map (if any), or the newly inserted value if there is none.

1 Like

In the specific case of HashMap, the entry API is the right answer.

However, the same problem can occur in other cases. Here you can usually work around it by performing the borrow twice. For example:

fn works(t: &mut HashMap<String, i32>) -> &i32 {
   let k = String::from("key");
   match t.get(&k)
       Some(v) => t.get(&k).unwrap(),
       None => {
           ...
       }
   }
}

This would work because the borrow that is ultimately returned is not created until after it enters the Some branch, so the None branch is not included in "all possible execution paths from that borrow".

Sometimes there are entirely alternate rewritings you could do as well:

fn works(t: &mut HashMap<String, i32>) -> &i32 {
   let k = String::from("key");
   if !t.contains_key(&k) {
      t.insert(k.clone(), 1);
   }
   t.get(&k).unwrap()
}

But again, the entry API is the right answer for HashMap.

1 Like