My background in C++ makes me uncomfortable about interior mutability. The code below is my investigation around this topic.
I agree that, from the borrow checker point of view, dealing with many references on every single struct which internal state could be altered soon or later is impossible; that's clearly where interior mutability can help.
Moreover, in chapter 15.5 "RefCell and the Interior Mutability Pattern" of The Rust Programming Language, the example
about the Messenger
trait and its implementation on the
MockMessenger
struct makes me think that it is a common API
design to systematically prefer &self
over &mut self
even
if its quite obvious that some kind mutability will be mandatory
soon or later.
How could an implementation of Messenger
not alter its internal
state when sending a message?
The exception is just printing the message, which is consistent
with &self
, but the general case would probably consist in
writing to some kind of inner stream, which could imply buffering,
updating error flags...
All of this certainly requires &mut self
, as for example
impl Write for File
.
Relying on interior mutability to solve this problem sounds to me
like, in C++, const_cast
ing or abusing of mutable
members just
because elsewhere in the application we were not consistent about
const
ness (common mistake for learners of C++).
So, back to my example code below, should I:
&mut self
(the compiler doesn't complain, even if it's
not mandatory) from change_e()
to change_i()
in order to
keep consistent with the fact that I alter the values of the
stored integers?&self
, because interior mutability allows it, even
if I actually alter the values of the stored integers?This decision is not only local to the struct itself but will have a large influence on what could be expressed in the application using this struct. The second solution will certainly help a lot, because only shared references are involved, but is it consistent with what is expected in Rust.
I cannot find an answer to this question in Rust API Guidelines. Is there any other Rust documentation similar to C++CoreGuidelines?
/*
$ rustc int_mut.rs && ./int_mut
initial: 1 2 3 4 5 6 7 8 9
change_a: 11 2 3 4 5 6 7 8 9
change_b: 11 22 3 4 5 6 7 8 9
change_c: 11 22 33 4 5 6 7 8 9
change_d: 11 22 33 44 5 6 7 8 9
change_e: 11 22 33 44 55 6 7 8 9
change_f: 11 22 33 44 55 66 7 8 9
change_g: 11 22 33 44 55 66 77 8 9
change_h: 11 22 33 44 55 66 77 88 9
change_i: 11 22 33 44 55 66 77 88 99
*/
struct Thing {
a: i32,
b: std::boxed::Box<i32>,
c: std::rc::Rc<i32>,
d: std::sync::Arc<i32>,
e: std::sync::Mutex<i32>,
f: std::sync::RwLock<i32>,
g: std::cell::UnsafeCell<i32>,
h: std::cell::Cell<i32>,
i: std::cell::RefCell<i32>,
}
impl Thing {
fn new() -> Self {
Self {
a: 1,
b: std::boxed::Box::new(2),
c: std::rc::Rc::new(3),
d: std::sync::Arc::new(4),
e: std::sync::Mutex::new(5),
f: std::sync::RwLock::new(6),
g: std::cell::UnsafeCell::new(7),
h: std::cell::Cell::new(8),
i: std::cell::RefCell::new(9),
}
}
fn show(&self) -> String // & is enough (read-only)
{
format!(
"{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
self.a,
self.b,
self.c,
self.d,
self.e.lock().unwrap(),
self.f.read().unwrap(),
unsafe { *self.g.get() },
self.h.get(),
self.i.borrow(),
)
}
fn change_a(&mut self) // &mut is mandatory
{
let target = &mut self.a;
*target += 10;
}
fn change_b(&mut self) // &mut is mandatory
{
let target = self.b.as_mut();
*target += 20;
}
fn change_c(&mut self) // &mut is mandatory
{
let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
*target += 30;
}
fn change_d(&mut self) // &mut is mandatory
{
let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
*target += 40;
}
fn change_e(&self) // !!! no &mut here !!!
{
// With C++, a std::mutex protecting a separate integer (e)
// would have been used as two data members of the structure.
// As our intent is to alter the integer (e), and because
// std::mutex::lock() is _NOT_ const (but it's an internal
// that could have been hidden behind the mutable keyword),
// this member function would _NOT_ be const in C++.
// But here, &self (equivalent of a const member function)
// is accepted although we actually change the internal
// state of the structure (the protected integer).
let mut target = self.e.lock().unwrap();
*target += 50;
}
fn change_f(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e)
let mut target = self.f.write().unwrap();
*target += 60;
}
fn change_g(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e, f)
let target = self.g.get();
unsafe { *target += 70 };
}
fn change_h(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e, f, g)
self.h.set(self.h.get() + 80);
}
fn change_i(&self) // !!! no &mut here !!!
{
// actually alters the integer (as with e, f, g, h)
let mut target = self.i.borrow_mut();
*target += 90;
}
}
fn main() {
let mut t = Thing::new();
println!(" initial: {}", t.show());
t.change_a();
println!("change_a: {}", t.show());
t.change_b();
println!("change_b: {}", t.show());
t.change_c();
println!("change_c: {}", t.show());
t.change_d();
println!("change_d: {}", t.show());
t.change_e();
println!("change_e: {}", t.show());
t.change_f();
println!("change_f: {}", t.show());
t.change_g();
println!("change_g: {}", t.show());
t.change_h();
println!("change_h: {}", t.show());
t.change_i();
println!("change_i: {}", t.show());
}
Interior mutability is a design pattern in Rust that allows you to mutate data even when there are immutable references to that data; normally, this action is disallowed by the borrowing rules.
A RefCell is another way to change values without needing to declare mut . It means "reference cell", and is like a Cell but uses references instead of copies.
Fortunately for us, the Rust standard library provides two wrappers, std::cell::Cell and std::cell::RefCell, that allow us to introduce interior mutability in externally immutable instances of data structures. With Cell<T> and RefCell<T> in our collective toolbelts, we can harness the power of interior mutability.
The unspoken heuristic is that avoiding mutability when possible is good. And yet, in some cases you need a few mutable fields in data structures, whether they’re mutable or not.
A thriving API abuse permits hackers to achieve admin-like access to the targeted API. This access endows hackers to make API work as per their will. Hackers make use of existing API vulnerabilities to rob crucial private or business information while corrupting your websites or applications.
Without interior mutability, you would be forced to always use mutable pointers, which would allow mutation of the inner value and may be undesired. For instance, consider the following naive reference counted wrapper:
Relying on interior mutability to solve this problem sounds to me like, in C++,
const_cast
ing or abusing ofmutable
members just because elsewhere in the application we were not consistent aboutconst
ness (common mistake for learners of C++).
This is a completely understandable thought in the context of C++. The reason it isn't accurate is because C++ and Rust have different concepts of mutability.
In a way, Rust's mut
keyword actually has two meanings. In a pattern it means "mutable" and in a reference type it means "exclusive". The difference between &self
and &mut self
is not really whether self
can be mutated or not, but whether it can be aliased.
In the Messenger
example, well, first let's not take it too seriously; it's meant to illustrate the language features, not necessarily system design. But we can imagine why &self
might be used: Messenger
is meant to be implemented by structures that are shared, so different pieces of code can hold references to the same object and use it to send
alerts without coordinating with each other. If send
were to take &mut self
, it would be useless for this purpose because there can only be one &mut self
reference in existence at a time. It would be impossible to send messages to a shared Messenger
(without adding an external layer of interior mutability via Mutex
or something).
On the other hand, every C++ reference and pointer can be aliased.¹ So in Rust terms, all mutability in C++ is "interior" mutability! Rust has no equivalent to mutable
in C++ because Rust has no const
members (the catchphrase here is "mutability is a property of the binding, not the type"). Rust does have an equivalent to const_cast
, but only for raw pointers, because it's unsound to turn a shared &
reference into an exclusive &mut
reference. Conversely, C++ has nothing like Cell
or RefCell
because every value is implicitly behind an UnsafeCell
already.
So, back to my example code below, should I[...]
It really depends on the intended semantics of Thing
. Is it the nature of Thing
to be shared, like a channel endpoint or a file? Does it make sense for change_e
to be called on a shared (aliased) reference? If so, then use interior mutability to expose a method on &self
. Is Thing
primarily a container for data? Does it sometimes make sense for it to be shared and sometimes exclusive? Then Thing
should probably not use interior mutability and let the user of the library decide how to deal with shared mutation, should it be necessary.
¹ Actually, C++ does have a feature that makes pointers work similar to references in Rust. Kind of. restrict
is a non-standard extension in C++ but it's part of C99. Rust's shared (&
) references are like const *restrict
pointers, and exclusive (&mut
) references are like non-const
*restrict
pointers. See What does the restrict keyword mean in C++?
When was the last time you deliberately used a restrict
(or __restrict
, etc.) pointer in C++? Don't bother thinking about it; the answer is "never". restrict
enables more aggressive optimizations than regular pointers, but it is very hard to use it correctly because you have to be extremely careful about aliasing, and the compiler offers no assistance. It's basically a massive footgun and hardly anyone uses it. In order to make it worthwhile to use restrict
pervasively the way you use const
in C++, you'd need to be able to annotate onto functions which pointers are allowed to alias other ones at which times, make some rules about when pointers are valid to follow, and have a compiler pass that checks whether the rules are being followed in each function. Like some kind of... checker.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With