I did not say, that &mut
is useless. I just said, it is not as useful as it is for multi-threaded code and that is logically correct, due to the reasons I've already stated. However, I didn't elaborate why it matters, so let me do that, now.
Your example is good for showcasing what would happen, if the immutable borrows, that we use today, would be able to mutate, as well and how that simply can't work. However, it leaves the question of, how a third, new type of borrow might behave, open. For what I imagine, it'd be one that inherits the property to be shared from &
and the property to mutate from &mut
, but is neither Sync
nor Send
. &
and &mut
would still behave as they do today.
Before diving into that mysterious new borrow type, lets examine the problem &mut
is trying to solve: it is that of changing lifetimes. Whenever you mutate a value, that contains a unique pointer, the implicit lifetime of the unique pointer is over and the implicit lifetime of the new unique pointer begins. Any non-unique pointers to the value the unique pointer was pointing to become dangling. This is what happens when you have a pointer to one of the elements of a vector and you push new elements to the vector. It has to reallocate memory and may invalidate the old memory.[1]
As already mentioned in this thread, the pointers do not automatically change their value to the new memory address. What &mut
does is, it makes sure that no other non-unique pointers exist at the time it was created. For that to work, we have to attach explicit lifetimes to pointers. This is the difference between &
and const *
.[2]
Without explicit lifetimes and &mut
in particular, Vec
would be impossible to implement without a RefCell
. That would, in return, transform every lifetime error into a runtime error, while both consuming more memory and being slower. No one would want to work with a new abstract language, that is just another worse-performing C without any outstanding benefits. We have plenty of those, already.
This is where &mut
comes into play. Due to its guarantee of being the only pointer to a piece of memory, you can reallocate memory without having to worry about leaving dangling pointers behind, unless you messed up your own implementation in an unsafe code block.
This also implicitly answers the question above. What would be so bad about simply mutating some integer on the stack through an immutable borrow. The correct answer is: Nothing. There is no reason for splitting borrows into immutable and mutable for this simple task, but we don't have &
and &mut
for the simple tasks, we have them for the difficult ones. Using Cell
is a zero-cost abstraction for what you want to achieve and we would rather add a bit of verbosity to the simple cases than having to deal with headaches for the complex ones. This is the trade-off Rust chose and IMO, it's worth it.
Getting back to the mysterious new borrow type. A single-threaded version of &mut
could work like RefCell
in debug mode, but be proven to work correctly during compile time when using optimizations for a stable build and replace the RefCell
(includes runtime checks) with an UnsafeCell
(no runtime checks). This would obviously not work for all situations, but in those cases where it cannot be proven, the compiler would emit an error and you'd have to either use the &mut
we all know or RefCell
, if the compiler can't reason about that, as well.
Now, why would this only work for single-threaded, but not for multi-threaded code, you may ask. From a logical point of view, this could also work for multi-threaded code, but due to how multi-threading can shuffle code execution between threads in any possible way, the cases to test for correctness grow exponentially with code size and at some point become practically impossible to check during compilation.
In a nutshell, the advantage of the new borrow type is, that it works like RefCell
, but without the added runtime cost of RefCell
. It'd help getting rid of manual optimizations, where RefCell
is ditched in favor of UnsafeCell
without having to resort to unsafe code.
[1] Technically speaking, reallocation may return the same memory address, if it finds enough free space after the already allocated space to grow to the new size. In practice, you have no other choice to assume, that the worst-case will happen, i.e. new memory is allocated with the new size, values are copied from the old to the new space and the old memory deallocated.
[2] After compilation is done, there is no difference between &
and const *
. Only during compilation are lifetimes tracked for &
, but not for const *
.