Yet another “cannot borrow `*self` as mutable because it is also borrowed as immutable”

Seems like this should be a fairly normal thing - an instance method that mutates state calls a method that does not and another one that does. The twist here is the non-mutating method saving a reference to the state.

struct Problem {
    level:i32
}

impl Problem {
    fn f3(&mut self) {
        self.level +=1;    
    }
    
    fn f2<'a>(&'a self, v: &mut Vec<&'a i32>) {
        v.push(&self.level);
    }
   
    fn f1(&mut self){
        let mut v:Vec<&i32> = Vec::new();
        self.f2(&mut v);
        self.f3();
        self.f2(&mut v);
    }
}

Remove all lifetime annotations and use an owning Vec. I don't think you want to store references to level. (which would mean your Vec only has one distinct value at any one time if it did, indeed if that was even possible)

5 Likes

I was gonna say that often splitting things into multiple methods, each taking a reference to the whole of self often tends to lead to borrow checking errors in cases where the code is logically sound, i.e. the code would compile if the method definitions were inlined… and that there’s some desire even to improve the situation eventually… but in this case the code is just clearly wrong!

The reference to self.level that’s pushed to the Vec in the first .f2 call can no longer be valid once self.level is updated, so the operations here violate the most basic principles, a value is mutated while an immutable reference to it still exists. I mean… the code here does not actually dereference it, so the situation could be even worse, but a Vec cannot contain references of different lifetime, so by the time of the second reference being put into the Vec, the types simply cannot check out, since both references pushed into the Vec cannot be valid at the same time, since one was taken before the mutation and one was taken after the mutation.

6 Likes

The code is oversimplified. I do want to store references. The real state is more complex and I’d like to avoid copying it. Owning vec won’t work.

This came up when rewriting some C++ for graph traversal. The state there is a container and the vec that is passed around stores pointers to items in that container (building a path).

I understand what the rust issue is, just not sure what an efficient way to solve it would be?

Store something without a lifetime. Maybe this means wrapping things in Rc or Arc, maybe this means storing some indices that can be used to "refind" the state and the thing in it.

1 Like

Rust references aren't for not-copying, they're for not-owning. For sharing data without copying or passing data by reference Rust has other types to choose from, which support different ownership.

Using references makes the Vec a temporary read-only view of Problem, and it becomes impossible to use it outside of the scope of function where Problem has been created. That's what temporary references do.

If you just want to avoid copying the underlying data, then Arc is the correct type.


In your case &i32 guarantees it won't change, but f3(&mut self) allows changing it. That's a conflict. You could use Cell<i32> or Mutex<i32> and f3(&self) to change the level while it is being shared. If you need such shared mutability, Arc<Mutex<T>> is the common way to achieve it. It's normal in Rust to use such types that expose ownership and mutability.

5 Likes

Yes. Rc<RefCell<T>> all the way. Thanks.

Somewhat interesting (and perhaps slightly misleading given the exact error message) is that rust is ok with calling f3() after f2(&mut v) (i.e. - not complaining with the &i32 being placed in the vec and mutated after that). It only starts complaining after the second call to f2(&mut v) is added.

Yes, it is weird that it compiles without the last call. You've run into a special case of a special case!

The borrow checker is able to implicitly shorten lifetimes if they're not used later. In this case it shortens lifetime of the Vec's loan to end before the call to f3(), because this loan is not used after the call. So that makes the lifetimes not overlap.

Normally that works only for simple cases like local variables, and would still be invalid for a collection with Drop, because the Drop function is implicitly called later could observe the broken mutated &i32 reference. However, the Vec type is special, and uses a libstd-private feature that allows it to promise not to touch the invalid references when dropping, which makes it technically safe: Drop Check - The Rustonomicon

unsafe impl<#[may_dangle] T, A: Allocator> Drop for Vec<T, A> {
4 Likes

Story of my life…

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.