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_casting or abusing of mutable members just
because elsewhere in the application we were not consistent about
constness (common mistake for learners of C++).
So, back to my example code below, should I:
- use
&mut self(the compiler doesn't complain, even if it's not mandatory) fromchange_e()tochange_i()in order to keep consistent with the fact that I alter the values of the stored integers? - keep using
&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());
}
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
mutkeyword actually has two meanings. In a pattern it means "mutable" and in a reference type it means "exclusive". The difference between&selfand&mut selfis not really whetherselfcan be mutated or not, but whether it can be aliased.In the
Messengerexample, 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&selfmight be used:Messengeris meant to be implemented by structures that are shared, so different pieces of code can hold references to the same object and use it tosendalerts without coordinating with each other. Ifsendwere to take&mut self, it would be useless for this purpose because there can only be one&mut selfreference in existence at a time. It would be impossible to send messages to a sharedMessenger(without adding an external layer of interior mutability viaMutexor 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
mutablein C++ because Rust has noconstmembers (the catchphrase here is "mutability is a property of the binding, not the type"). Rust does have an equivalent toconst_cast, but only for raw pointers, because it's unsound to turn a shared&reference into an exclusive&mutreference. Conversely, C++ has nothing likeCellorRefCellbecause every value is implicitly behind anUnsafeCellalready.It really depends on the intended semantics of
Thing. Is it the nature ofThingto be shared, like a channel endpoint or a file? Does it make sense forchange_eto be called on a shared (aliased) reference? If so, then use interior mutability to expose a method on&self. IsThingprimarily a container for data? Does it sometimes make sense for it to be shared and sometimes exclusive? ThenThingshould probably not use interior mutability and let the user of the library decide how to deal with shared mutation, should it be necessary.See also
¹ Actually, C++ does have a feature that makes pointers work similar to references in Rust. Kind of.
restrictis a non-standard extension in C++ but it's part of C99. Rust's shared (&) references are likeconst *restrictpointers, and exclusive (&mut) references are like non-const*restrictpointers. 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".restrictenables 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 userestrictpervasively the way you useconstin 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.