Why am I getting this error (E0502)?

I am getting this error:

error[E0502]: cannot borrow `*self` as immutable because it is also borrowed as mutable
    --> src/system.rs:1928:28
     |
1909 |                 let car: &mut Car = self.cars.get_mut(Cx).unwrap();
     |                                     --------- mutable borrow occurs here
...
1928 |                         if self.FixedRouteMirrorCheck(car,self.industries
     |                            ^^^^                       --- mutable borrow later used here
     |                            |
     |                            immutable borrow occurs here

FixedRouteMirrowCheck has this signature. None of its parameters are mutable. Why is the compiler complaining? FixedRouteMirrorCheck just does some checking and does not modify anything (but is does need the self parameter to allow looking some things up.

 fn FixedRouteMirrorCheck(&self,car: &Car,Ix: Option<&Industry>) -> bool {...}   

The full crate is here: GitHub - RobertPHeller/freight_car_forwarder: Freight Car Forwarders in rust

In line 1909, you define car:

let car: &mut Car = self.cars.get_mut(Cx).unwrap();

Later, you use a similar function, IndustryTakesCar:

                    if Self::IndustryTakesCar(self.industries
                                    .get(&MirrorInd),&car)

and I note that you have downgraded your mutable reference to an immutable one. Maybe try the same here?

if self.FixedRouteMirrorCheck(&car, self.industries

&mut _ are exclusive references. Think of the mut spelling as "mutually exclusive", like a Mutex.[1]

Here, self.cars is exclusively borrowed for as long as car is in use.

let car: &mut Car = self.cars.get_mut(Cx).unwrap();
// (Side note, you could `... = &mut self.cars[Cx];` instead)

Then here, you try to share-borrow all of self while keeping car alive:

 if self.FixedRouteMirrorCheck(car, self.industries.get(&MirrorInd))

But you can read self.cars from that shared-borrow of self, so that conflicts with keeping the exclusive-borrow of self.cars alive -- so you can't pass in car.


To reiterate, when you have a signature like this:

// (Simplified from the original for ease of illustration)
pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { ... }

// Without lifetime elision:
// fn get_mut<'a>(&'a mut self, index: usize) -> Option<&'a mut T> { ... }

The meaning of the API is "so long as the return value is in use, the exclusive input borrow remains active". As of yet, there is no mechanism to downgrade the exclusive input borrow -- the exclusive borrow of self.cars in the CarAssignment method -- to a shared borrow, so long as the return value is in use.[2]


How to fix? Pass in an index or a fresh, shared borrow instead.

if self.FixedRouteMirrorCheck(&self.cars[Cx], self.industries.get(&MirrorInd))

If you need to mutate the Car after this, you'll need to create a new borrow.


  1. Most learning material will tell you it's "mutable vs immutable", but "exclusive vs shared" is more accurate. The exclusivity is a fundamental property, and shared ("internal") mutability data structures like Mutex, which can enable mutating through a &, exist. ↩︎

  2. And if we get one, it will probably happen at the call site, not later on. ↩︎

6 Likes

Question does the fresh shared borrow include the updates done to the Cx element of cars?

Does anyone know why Rust doesn't handle this automatically though? I thought Rust automatically reborrowed references at call sites. It's observable, because sometimes the original reference gets "locked" for the lifetime of the reborrowed reference.

    let mut s = "hello\n".to_string();
    let myref = &mut s;
    let r = borrow_substr(myref); // Rust implicitly reborrows myref with a shorter lifetime
    myref.push('!'); // error: can't use myref mutably during reborrow
    println!("{r}");
    myref.push('!'); // but now is fine

(playground)

In Robert's case, it sounds like people are saying an explicit reborrow will do the trick. But then why doesn't Rust's implicit reborrow do it?

Yes. A reference isn't a copy of the data; it refers to the (previously modified) car.

A reborrow is a borrow derived from and dependent on some existing borrow. Reborrows never release any existing borrow. @quinedot is suggesting borrowing cars a second time with only shared access, so that the first, exclusive borrow is not used further and will be released.

3 Likes

When you are mutably borrowing it. you can't borrow it .

Arg... Rust seems to enforce writing code that appears to be sub-optimal and/or needlessly verbose -- one hopes the compiler is optimizing things. Basically you really can't use references (mutable or otherwise) with helper functions and you can't even move values from one field to another in a sub-structure (one needs to fetch the value being moved to a local temp variable). One can only realistically pass indexs to struct elements of vectors or other collections (eg HashMaps) to helper functions. Very fustrating.

1 Like

… Since I don’t think that’s true, I feel more comfortable and familiar with it over time…
P/s:prohibition will prevent you point to something like null ptr. because mut ref can change value in ref so you don't know what would be changing.

Rust does this on purpose.

The idea is that signature of the function — not the body — defines requirements of the function. It's an abstraction layer.

And &mut self means borrowing ALL of self with a strict guarantee that there cannot be any other usable reference to anything inside self.

These guarantees are useful for library stability, and exclusivity is necessary for many safety guarantees.

It's just that this combo is super annoying for getters. Rust lacks ability to borrow less than all fields in &mut self methods. The potential feature for this would be "view types". In the meantime, there are alternatives like splitting structs into independent subsets, passing individual fields to "static" methods without self, or using &self methods to mutate (via interior mutability).

4 Likes

A trick I (rarely) use is borrowing parts of a struct and then returning mutable / immutable views of the parts, wrapped in structs containing (mutable / immutable) references to parts of the original data structure. Here without wrapping structs for brevity:

1 Like

That is sometimes called a "view struct", and is a manual approximation of the "view type" idea.

2 Likes

Well, yeah. But it does this so that later, when you add a line to the function that modifies self.cars between the two uses, and that causes the hash table (or whatever it is) to be reallocated, you don't then dereference car, which is now a dangling pointer. It's not crazy.

Passing the whole world around by pointer, modifying it with one hand while holding on to pointers to parts of it with the other ... it really is kind of a bad idea.

4 Likes

Well, the original C++ code had vectors and maps of pointers. And the vectors and maps were never going to be reallocated during the course of this partitular method. And the only vector that ever changed size was the cars vector, but that happened in a completely separate part of the program. Everything else was "static" once read in from the data files.

Then you might want to factor out everything static to one struct and the cars vector to another, to immutably borrow the former as much as you want?

1 Like

Oh, I see! Then it's probably easiest to just change FixedRouteMirrorCheck to take its first argument as Cx: usize instead of car: &mut Car.

Rust's safety story is a big package deal. You get the bad with the good. The bad is, you can't alias freely. It makes you design your data structures a little differently. You won't be doing a straight line-for-line port of your C++ (if safety was that easy, we wouldn't need Rust!). The good is, things don't alias anymore! A non-mut reference points to immutable data, it's not a view on data that might be changing. Methods that set things don't need to watch out for the x = x case the way operator= has to in C++. Iterators and references can't be invalidated like in C++.

1 Like

Rust seems to enforce writing code that appears to be sub-optimal and/or needlessly verbose

Well, if you want to do something that the compiler can't prove to be safe, you have to tell it, "It's fine, I know what I'm doing, trust me", and you do it using raw pointers and unsafe functions - but that isn't needless verbosity.

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.