[Idea?] Access state for borrowing

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 C restrict 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> and RefCell<T> is UnsafeCell<T>- a T 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++.

11 Likes