Immutable borrow from mutable self prevents further immutable borrow of self

use std::collections::HashMap;
#[derive(Default)]
struct Foo {
    map: HashMap<String, String>,
}

fn calc_value() -> String {
    "ololo".into()
}

impl Foo {
    pub fn get_or_create(&mut self, key: String) -> &str {
        self.map.entry(key).or_insert_with(calc_value)
    }
    
    pub fn print(&self, value: &str) {
        println!("{}", value)
    }
}

fn main() {
    let mut foo = Foo::default();
    let value = foo.get_or_create("key".into());
    foo.print(value);
}

error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
  --> src/main.rs:24:5
   |
23 |     let value = foo.get_or_create("key".into());
   |                 --- mutable borrow occurs here
24 |     foo.print(value);
   |     ^^^       ----- mutable borrow later used here
   |     |
   |     immutable borrow occurs here

This code gives error, because I use reference(value), derived from mutable borrow of self(let value = foo.get_or_create("key".into());) when I want to get immutable borrow of self (foo.print(value);). AKAIK it is because lifetime of mutable borrow of self is prolnged until derived reference (returned from get_or_create) is alive. I don't understand the reason behind that, especially why prolonged borrow is considered mutable, while derived reference is immutable. Is it just limitation of current implementation of borrowchk or there is some unsoundness in doing &mut self -> &self conversion here unconditionally?

1 Like

&mut T doesn't mean mutable borrow and &T doesn't mean immutable borrow. It is unique and shared borrows respectively.

https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html

The reason for why this is disallowed is because of the presense of shared mutability. For example Mutex::get_mut gives a reference to the guarded value without locking the mutex.

So if you could get aliasing unique references like so if this were allowed

fn get_ref(m: &mut Mutex<T>) -> &T {
    m.get_mut()
}

let a = get_ref(&mut mutex);
let b = mutex.lock();
// a aliases b here, which would be UB

But this doesn't compile

3 Likes

The two borrowed types in this function signature have the same lifetime, which we can see more clearly if we apply the lifetime elision rules:

pub fn get_or_create<'a>(&'a mut self, key: String) -> &'a str

This means that the &mut self borrow passed in must live as long as the &str value that gets returned.

There's currently no way to say "this function borrows self mutably/exclusively for one lifetime, and immutably/shared for a different lifetime." This is a known limitation of lifetime syntax.

5 Likes

The function takes a &mut self, so self will be mutably borrowed for the required lifetime, which must be as long as the return value lives.

The compiler does not and cannot "downgrade" borrows like you propose.

The solutions are:

  1. Create and call a second function that takes &self and does the lookup again from scratch, and then use the reference from that instead of the one from get_or_create.
  2. Do such a second lookup in get_or_create and then return both the new reference and a copy of self with type &Self, then in the caller use a let statement to override the binding with the new return value
  3. Restructure the code so you don't need to access the struct while holding the reference to the value

Ah, I see, seems that interior mutability breaks my idea. Just for clarity and for anybody who will read that thread: your example will not compile only if a is used after mutex lock, for example it that code

let a = get_ref(&mut mutex);
let b = mutex.lock();
drop(a)

Otherwise, compiler will drop a reference before lock, thanks NLL

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.