Finding it hard to come up with use cases for interior mutability via Cell<T> and RefCell<T>

I will like to think I understand the idea of Cell<T> and RefCell<T>. By default Rust compiler only allows one exclusive &mut T at a time in scope. Cell<T> and RefCell<T> helps bypass this. I have written small code snippets to prove how Cell<T> and RefCell<T> works.

What I am failing to understand now is the real life use cases of this functionality. Why will I ever want to use Cell<T> and RefCell<T> in a code base? What patterns does it make possible, what problems does it solve?

Something tells me I'll appreciate them more if I understand a bit more of what interior mutability provides.

But for now I am coming up short. Will appreciate if someone can make clear the whole idea behind interior mutability, and specifically the real life uses (not tutorial level code examples) for smart pointers like Cell<T> and RefCell<T>

Maybe you'll have an easier time understanding the practical use of Mutex in std::sync - Rust. It's also an interior mutability primitive. Once you have understood Mutex, you could look at RwLock in std::sync - Rust which is a “generalization” of Mutex, allowing more flexible access patterns (at the cost of some more runtime checks, and a more restrictive Sync bound).

Finally, you can see RefCell as a single-threaded RwLock that panics on deadlocks. (In a single-threaded context, every lock is a deadlock, so it always panics instead of ever locking.) The advantage over Rwlock is less runtime overhead bevause no thread safety is needed, and no deadlock possibility, and also if you don't want your data to be modified from multiple threads, it enforces this, so reasoning about code might also become easier.

Regarding Cell, (most of) its use-cases could generally be covered by RefCell, too. It uses API restrictions instead of counters and guard objects to avoid “data races”. So the advantage (in the cases where it's applicable) over RefCell are mostly just better performance; but also, it offers some special API relying on the lack of run-time checks to allow things such as converting &mut T to &Cell<T> or &Cell<[T]> to &[Cell<T>].

3 Likes

You might find some value in this article and the ones it links to in turn.

5 Likes

One use case is in a scenario I had like this:

fn foo(f:impl FnMut(i32), g: impl FnMut(i32){
    /* imagine the two functions getting called here */
}
fn main(){
    let counter = Cell::new(0);
    foo(|n| counter.set(counter.get() + n), |n| counter.set(counter.get() - n);
    println!("{}", counter.get());
}

Obviously a little contrived, but if you need to share state between closures interior mutability or channels are your only options, and channels are often overkill or more rarely unsuitable for such situations.

1 Like

This article comes up on google when searching circular mutable references. The author uses Rc/RefCell to implement a tree of Nodes. I have not thoroughly reviewed this article, just placing it here as a real world example of needing Rc/RefCell
Rust data structures with circular references

Also these types are perfect example of that old In theory, theory and practice are the same, but in practice, they are not adage.

In theory C++ and Rust provide very similar facilities (Box is called std::unique_ptr while Arc becomes std::shared_ptr), but there are no analogue for Cell and RefCell.

This is because they are not needed and dangerous. They are not needed because they only exist to provide safety — but C++ is not about safety thus it's not needed, but they are also dangerous because you can easily try to use them from different threads and then, without proper locking you would have nasty bugs.

It Rust you can safely write code that's not thread-safe! That means that Rc+RefCell are perfectly usable even if you are dealing with multithreading environment!

Compiler would ensure that you would never be able to use them from other thread, even by accident… which means that while, in theory, RefCell just adds pointless (from C++-developer POV) overhead in practice it allows you to write code which is both safe and fast!

On Zen4 (latest AMD architecture) atomic increment is 24 times slower than non-atomic (and on some older CPUs difference is even larger).

2 Likes

To expand on this, there is no analogue to Rc either, I believe. And regarding the thread safe alternatives to RefCell, those do exist again, std::mutex - cppreference.com for Mutex and std::shared_mutex - cppreference.com for RwLock. Well, except they don't guard any contained object.

Which, again, is something that can be explained by the fact that such a design could only provide _partial “safety” and thus possibly a false sense of security: due to the lack of a lifetime system for references, it could only enforce the mutex to be locked before accessing the contents starts but not enforce it to stay locked until after access to the contents ends.

1 Like