Need help with lifetimes

Here is the minimal version of the code that I am working with -

use std::collections::HashMap;

struct A {
    hm: HashMap<String, String>,
}

impl A {
    fn new() -> Self {
        let mut hm = HashMap::new();
        hm.insert(String::from("key"), String::from("value"));
        A { hm }
    }
    
//  I need this to take "&mut self" as in the real code it gets the "key" by modifying one of the fields of the struct A
    fn a(&mut self) -> Option<&String> {
        self.hm.get("key")
    }
    
    fn b(&mut self, _s: Option<&String>) {
        todo!();
    }
    
    fn c(&mut self) {
        let s = self.a();
        self.b(s);
    }
}

playground link

Trying to compile it with returns with the error message

error[E0499]: cannot borrow *self as mutable more than once at a time
--> src/lib.rs:24:9
|
23 | let s = self.a();
| -------- first mutable borrow occurs here
24 | self.b(s);
| ^^^^^^^-^
| | |
| | first borrow later used here
| second mutable borrow occurs here

I need to pass the immutable reference to one of the values stored in self.hm to self.b.
I'd like to understand why the current code fails to compile and how I can use lifetime annotations to convince the borrow checker that I am doing the right thing.
Thanks!

The reason that code doesn't compile is that you could violate guarantees that shared references provide trivially with it.

If I modify your code so that b changes the same key in the HashMap, then the reference a returned will now either be suddenly pointing to a new value. In the worst case, if the HashMap reallocated it's backing storage it could be pointing to memory that no longer belonged to it.


fn a(&mut self) -> Option<&String> {
    self.hm.get("key")
}

fn b(&mut self, _s: Option<&String>) {
    // Change the value pointed to by `a`'s return value
    self.hm.insert("key".into(), "new_value".into());
}

fn c(&mut self) {
    let s = self.a();
    self.b(s);
    // Now `s` is pointing at a different value!
}

The correct way to do what you need will vary depending on what your actual code needs to do. One option is to split your types so that you can just take a mutable reference to the hash map, and have b take only references to the other values it needs.

struct A {
    hm: HashMap<String, String>,
    something_else: Vec<usize>,
}

fn a(hm: &mut HashMap<String, String>) -> Option<&String> {
    hm.get("key")
}

fn b(_s: Option<&String>, _something_else: &mut Vec<usize>) {
    todo!()
}

fn c(&mut self) {
    let s = Self::a(&mut self.hm);
    Self::b(s, &mut self.something_else);
}
4 Likes

Hey @semicoleon!
Thanks for the explanation! I now understand the things that could possibly go wrong with my current method signatures and the rust borrow checker being conservative, rightly throws an error.
Your solution to split the types works well for my use case. I have decided to implement the methods a and b on two different types and have these types as fields of struct A.

But I am still having a hard time trying to decipher the compiler error message and how that translates to what you have just explained.

The compiler here talks about two overlapping exclusive references to self.

here I assumed that the mutable borrow because of self.a lasts only till the end of the line and the return value is just an immutable reference to a field in the struct and not the struct itself.

So, the mutable reference here isn't used when the mutable reference from the previous line is active.
My reasoning here is that because of NLL, lifetimes are not the same as function/block scopes.
So, when the compiler returns an error saying that there are two overlapping mutual references. i fail to see how that could be the case here. :sweat_smile: :sweat_smile:

I feel like I am missing something really trivial and once I identify the missing information everything will fall into place. But I can't quite point finger at what it is that I'm missing.

Hah, sorry I totally skipped over the error message!

I think making the lifetimes explicit can help clarify a bit. So I'll fill in a's signature with what the borrow checker sees

fn a<'s>(&'s mut self) -> Option<&'s String>

The explicit lifetime shows us that the lifetimes of those two references are connected somehow.

If we translate that function signature into casual English: we have a function that borrows a String from self and the lifetimes of those two references have to be "compatible". In this case "compatible" just means that the borrow of self has to be valid for at least as long as the string reference sticks around.

Rephrasing that slightly, self must be borrowed for as long as the String is borrowed.

The borrow checker can consider a value "borrowed" even if the reference that originally borrowed the value doesn't appear anywhere in the source code, which I think might be part of the confusion. You can think of it sort of like a reference count, where self gets 1 mutable borrow added to it's count when the method is called, but the count isn't decremented until the &String borrow ends since they're connected by that lifetime in the function signature.

3 Likes

This makes a lot of sense!!
So, is it possible to use the lifetime annotations to convey to the borrow checker that the lifetime of the returned value Option<&String> is actually dependent on the lifetime of the struct A that owns the HashMap which contains the String to which the return value holds the reference?

Something like...

fn a<'a>(&'a mut self) -> Option<&'s String> 
where 's: 'a 

i.e., 's oulives 'a.

But the reference itself is created within the function a. So I think the reference is bound by the lifetime of the function even though it holds reference to a value that outlives the function itself.
I have seen examples of people using std::mem::take or std::mem::replace (example) in such circumstances to sort of (rightly) extend the lifetime of the reference, so that it's no longer bound by that of the function in which it was created.

Yes! Thanks that's a better mental model to have than the last one!

I'm not sure I understand the question. You can use a lifetime from another part of the struct if you have a reference in the struct, yes.

This is mostly a nitpick, but it's not the lifetime of the function, it's the lifetime of a reference the function receives as a parameter

Thanks @semicoleon ! That answers my question.

This sounds like permissions in, permissions out, where you wish you could have a sort of two-phase borrow where you mutably borrow for the duration of the function call only, and upon return, immutably borrow for as long as the return value needs to be valid. As the link notes, that's a semantic change we can't make without some sort of opt-in.

The way I eventually resolved how Rust works today in my own mental model to internalize that this part of how lifetimes can't change -- they're the output of a static analysis -- so the &'s mut self you had to create in order to output the &'s String is an exclusive borrow for 's no matter what.

More generally, your &'s String is a reborrow of the &'s mut self, and the thing you reborrow from has to be valid for at least as long as your reborrow. "Permissions in, permissions out" would be more like two distinct, serial borrows.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.