Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"cannot borrow `*self` as immutable because it is also borrowed as mutable" -- does the compiler really need to be this strict?

Tags:

rust

I'm having some trouble understanding why the following bit of code makes the compiler unhappy:

let v = self.buckets.get_mut(self.get_bucket_index());

where buckets is a vector.

I do understand that separating this into two statements makes it compile:

let idx = self.get_bucket_index();
let v = self.buckets.get_mut(idx);

but my understanding of the original statement is that deep down it would be translated into something like:

let tmp = self.get_bucket_index();
let v = self.buckets.get_mut(tmp);

which is equivalent to the working implementation I showed above.

My question then is: why would the compiler not allow this? Is there any situation where accepting my original code could lead to bugs? Or is this just a crude, easier to implement approximation that the current compiler does but that could be relaxed in the near future (similar to what happened with lexical lifetimes)?

like image 720
devoured elysium Avatar asked Sep 16 '25 22:09

devoured elysium


2 Answers

Rust tries its best to preserve left-to-right execution, so first it turns your method call into a regular function call:

get_mut(&mut self.buckets, self.get_bucket_index())

And then it evaluates the arguments left-to-right. If you write that out naively, you get this:

let a = &mut self.buckets;
let b = self.get_bucket_index();
get_mut(a, b)

This will fail for the same reason your original attempt failed. When you separate it into two statements you're essentially changing the order in which a and b are bound.

let b = self.get_bucket_index();
let a = &mut self.buckets;
get_mut(a, b)
like image 188
drewtato Avatar answered Sep 19 '25 15:09

drewtato


@drewtato explained nicely what is the problem, but you also asked "couldn't the Rust compiler just allow that as this seems harmless?". Well, it could, and it does. Just not in your case.

There is a mechanism called Two-Phase Borrows that is intended to allow situations like that. It works by splitting a mutable borrow (not all of them, IIRC mainly in dot syntax) into two parts: "reservation" and "activation". In the reservation stage, the object is borrowed in assembly but the object is only considered immutably borrowed. This happens when the receiver expression is executed, prior to executing all other arguments. Then, when the method is actually called, the activation stage takes place, and it "upgrades" the shared borrow to a mutable borrow. This allows other immutable borrows to be taken between the phases, allowing the other arguments to reference the receiver immutably (I don't know why we couldn't define it in a way that allows mutating it too).

So why doesn't two phase borrows kicks in in your case? Because get_mut() isn't a method on Vec. It is defined on slices. Vecs of course Deref to slices, but this means we have to insert a call to Deref::deref() when borrowing, after reserving the reference. Deref::deref() is an arbitrary method, and can do arbitrary things with the reference, so we cannot use two-phase borrows here.

Of course, we could special-case Vec's Deref, but we want std code to be as little special as possible. Maybe one day we'll get a DerefPure annotation (if this will happen this will probably be for deref patterns), then we will be able to use this for two-phase borrows too.

like image 36
Chayim Friedman Avatar answered Sep 19 '25 15:09

Chayim Friedman