I’m reading the Rust book in my spare time for some time now and got to the topic of interior mutability.
Having a C++ background I know similiar concepts from C++ as well like const_cast or mutable and I’m really bugged by these things in C++ all the time because it let’s you do things that you wouldn’t expect from the public API.
Everytime I see constant methods in our code at work that uses some of these “hacks” as I call them to bypass the const mechanism it makes me sick because it just doesn’t feels right to do these things.
I also never got any real use case for these hacks until I read about interior mutability in the Rust book which showcases this feature in a unit testing-like scenario.
I never thought about this so far but it kind of makes sense to be able to do this in this specific scenario where the public API of some trait might get in the way.
Apart from that my mood got worse as soon as I read about Rust having these “hacks” too.
So what I really would like to know now is what real world usecases there might be other than the unit testing scenario mentioned that really need interior mutability and cannot be solved in other ways?
Internal caching/memoization is a prime use-case for interior mutability. When you have obj.get_foo() and you want to compute the value lazily and cache it internally without exposing that fact in the API (using &self, not &mut self). The way to “cheat” that is interior mutability.
Another place where I use it is in objects that are used in parallelized code. You can’t use &mut foo across threads at all, but you can easily and cheaply share &foo between threads (scoped ones, like in Rayon). So Mutex or RwLock in a struct allows having an object with all threading-friendly &self methods, but still some mutability.
BTW, they’re not hacks. They’re enforced to be correct at compile time. If the public API says something can be called with a shared reference, then it safely can.
foo = 3; // legal!
That’s because Rust doesn’t have concept of immutable memory. It only has concept of shared read-only or exclusive read-write access to memory (which is just as safe and IMHO is much more practical), and owned values that aren’t borrowed have exclusive access.
Usually shared vs exclusive access is statically decided by variable scopes and lifetimes, but interior mutability can switch between shared and exclusive access at run time (but it still guarantees you never get unsynchronized shared mutable access).
Rust has two reference types &T and &mut T. I find it helps if you don’t think of these as “immutable (or const) reference” versus “mutable reference” as is common in C++, but as “shared reference” and “exclusive reference”. If you have a &T, other code may also have that same &T. If you have a &mut T then that is exclusive; the type system guarantees no other code is looking at or interacting with T. This is the distinguishing characteristic between the two, more so than mutability.
Now consider AtomicUsize as an example, and specifically the method AtomicUsize::store which has the following signature.
In the C++ worldview you might see this method as taking a “const reference” to self and doing something dirty to mutate the underlying data through a const (immutable) reference.
In Rust I encourage you to see this method as taking a “shared reference” to self. Normally mutating a value would require an exclusive reference, but AtomicUsize employs the right synchronization to make the specific mutation performed by this method safe even with other code observing the same self, i.e. even with only a shared reference to self.
In summary: there is no such thing as const_casting an immutable reference to a mutable reference. But there is such thing as safely mutating through a shared reference by establishing mutual exclusion or synchronization in a variety of ways external to the type system.
UnsafeCell is the rust equivalent of mutable keyword. But should avoid direct use.
const_cast intent is backwards compatibility to APIs not written expecting cost type, you risk UB if trying to mutate the result.
To add to others in use; some tracer variables (eg counter) can sometimes be used, typically a debugging aid.
Ah yes caching makes sense of course. Haven’t thought of this because I usually either don’t have to cache stuff or do it in different ways that don’t require interior mutability in C++.
On the other hand it kind of makes sense that this works as you move the old data into the shadow variable.
Yes, I’m sure. You have to comment-out the // illegal line.