简体   繁体   中英

Interior mutability abuse in API design?

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 chapter15.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:

  • use &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?
  • 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());
}

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++).

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.

See also


¹ 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM