Rust compiler forcing "needlessly" verbose code?

I have somethis that I find weird (maybe it is just me). The rust compiler does not like it when I do this:

self.cars[Cx].SetTmpStatus(self.cars[Cx].LoadedP());

I get error[E0502]: cannot borrow self.cars as immutable because it is also borrowed as mutable

The "cure" seems to be:

{                                                           
    let temp = self.cars[Cx].LoadedP();                     
    self.cars[Cx].SetTmpStatus(temp);                       
}

Where:

    pub fn LoadedP(&self) -> bool {self.loadedP}                                
    pub fn SetTmpStatus(&mut self, p: bool) {self.tmpStatus = p;}

Is this really necessary? Or is an "unintented consequence" of the borrow checker?

It's an unintended consequence of evaluation order. Evaluation of expressions always proceeds left-to-right, so the evaluation order of your expressions puts the first self.cars[Cx] before the second. And, as soon as the first self.cars[Cx] has been evaluated, it’s holding a mutable borrow that cannot be released until after SetTmpStatus() is called.

8 Likes

It's a bit annoying, as magic makes the other way around work:

IIRC the rule is if the receiver is a mut ref it is first taken as a shared ref, then the arguments are evaluated, then it's upgraded to a mut ref.


Edit: wait, this example did have the mut ref on the receiver side... not sure what the difference is then?


Edit harder: ah, it's complaining about the borrow on self.cars specifically, the above trick doesn't work through multiple layers of ref. Maybe it could in some cases, but it seems tricky to prove it's safe. I think tree borrows are supposed to be able to handle this?

1 Like

The “magic” two-phase-borrows way to write this code is to perform a single mutable borrow, then use it twice:

let car = &mut self.cars[Cx];
car.SetTmpStatus(car.LoadedP());

This also has the advantage of not duplicating the work to look up .cars[Cx] even if the optimizer doesn’t figure out they’re equivalent. (I would consider this worth specifically doing if cars isn't a slice/vector, and probably not if it is, but I also think this is cleaner code regardless of performance.)

18 Likes

Chaining with indexing is actually the RFC's example of what two-phased borrows won't support.

That said, what is implemented is different from the RFC,[1] but is also not specified. Last I checked it wasn't even documented. Until they get specified, questions around two-phased borrows like "shouldn't this work" or "why does/doesn't this work" are sort of inherently :person_shrugging:.

Lack of a spec aside, tree borrows is a a proposed aliasing model. I don't think of proposed aliasing models as things which are supposed to extend what the language supports. But it is true that tree borrows are better suited to accept two-phased borrows as sound than stacked borrows is, in a general sense.[2]


  1. more things are accepted than was intended ↩︎

  2. or at least the original version of stacked borrows; Miri handles two-phased borrows in stacked borrows mode now, though IIRC that's possible because the model is no longer strictly stacked ↩︎

2 Likes

I don't this would work for my code, since a do this sort of thing multiple times, thus there would be multiple mutable borrows, which rust does not like at all/

The original C++ actually is cleaner... Of course the original original QBASIC was a typical BASIC plate of spaghetti. :slight_smile:

1 Like

This may not be applicable if your actual functions are more complex, but if you only need to get and set "simple" or whole values, you could wrap them in Cells, to not need &mut references. The trade-off is that you can't hold a reference to the value inside the cell, only to the cell itself.

It's also worth mentioning that "getters" and "setters" that don't enforce any invariants tend to be worse than just making the field itself public for this reason. It's of course a different situation if they need to be read-only or write-only, but using them religiously and unconditionally, as may happen when porting code, isn't going to be a fun time.

1 Like

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.