Rust's rules are a lot more specific than this, but the defaults and naming confuse a lot of people when they start looking into the details.
As @RustyYato mentioned, &T
and &mut T
are about "shared" vs "unique," not "immutable" vs "mutable." This has far deeper implications than perhaps you realize:
- A
&mut T
is, by virtue of being a unique reference, allowed to change the "shape" of an object. For example, it can change an enum variant, reallocate a vector or string, or deallocate a box. - These kinds of operations can invalidate other references into the object, causing them to dangle. This makes your access states proposal insufficient- it would also need to ensure that a given state does not invalidate the references used by another state.
- Even if you solve that problem, intuitive reasoning about
&mut T
is still insufficient, because&mut T
enables a lot of optimizations that can be invalidated in more subtle ways. It's treated much like a Crestrict
pointer.
RefCell
gets way more attention than it deserves in these kinds of discussions, because it's not a fundamental part of the Rust model. It is just a simplistic API that tries to push you back into the world of &T
/&mut T
as quickly as possible.
Coming from C++, what you really want is &Cell<T>
. This is a T
that can be mutated directly via shared references, with no dynamic checking or conversion back to &mut T
. This also has some subtle implications:
- Because unrestricted mutation à la
&mut T
can change an object's "shape," thus invalidating references to its interior,&Cell<T>
by default forbids forming those references to begin with, and only allows the entire object to be overwritten. - Some objects never change shape- arrays and structs always have the same sub-objects in the same place no matter how you overwrite them. This means it is safe to go from a
&Cell<[Element]>
to a&Cell<Element>
, or from a&Cell<Struct>
to a&Cell<Field>
, for example. - The real tool behind both
Cell<T>
andRefCell<T>
isUnsafeCell<T>
- aT
that the optimizer knows might be aliased, backing out of the optimizations mentioned above. You can build your own safe shared mutability tools on top.
Rust is still a little immature in this area. The &Cell<[Element]>
to &Cell<Element>
conversion is extremely recent, and the type system has no way to express &Cell<Struct>
to &Cell<Field>
. It's all there in the model, though, so you can wrap up an unsafe
implementation in a safe API and know it will work.
Given the tools above, this is more of a problem of ownership and lifetimes than mutability. The solution is just like any other container- you need to view the whole data structure together, and treat all its nodes as part of a single thing, rather than trying to use &'a T
for some impossible 'a
.
Rust basically gives you no tools for implementing this safely. You just need to bite the bullet and do it with unsafe
, then wrap it in a safe API. A language that did provide a safe way to do this would be far more complicated, probably requiring some general theorem proving features. Punting on that while still enabling safe wrapper APIs is still a huge gain over C++.