Design pattern for trial check


#1

I have a large struct and want to check if some undoable operation is possible.
So, the code might be,

impl LargeStruct {
    fn operate_something(&mut self) -> bool {}

    fn undo_something(&mut self) {}

    fn is_some_operation_possible(&mut self) -> bool { // I don't like this signature
        if self.operate_something() {
            self.undo_something();
            true
        } else {
            false
        }
    }
}

I don’ want to contaminate signatures of methods using this test method with mut.
Copy and test is an alternative but suppose a situation that undo is much cheeper than copy.

Are there any ways to eliminate mut signature from this test method?


#2

If &mut is necessary for operate_something and/or undo_something, then no.

You’re mutating the value; you can’t just wave that away. Rust isn’t like C/C++ where you can just hand-wave away constness and everything is OK.

The only place this rule doesn’t apply is with interior mutability on things like Cell and RefCell. But, if you were using that internally, you wouldn’t need &mut on the first two methods, either, and you’ll have to have to pay the runtime borrow checking costs.


#3

There is no runtime borrow checking cost for Cell. get() is just an inlined method that copies the value out, and set() is an inlined method that copies the value in (and the UnsafeCell::get() that they both call is an inlined function that just gets a pointer to the inner value).

After applying the inlining, these are exactly what would happen if you just accessed a field of a struct directly with stuct.field or struct.field = val.


#4

As @DanielKeep says, you can’t do this in Rust. If you have an &mut, that means you have exclusive access to an object. If you have an &, that means you have shared access. If someone else in another thread had an & reference while self.operate_something was running, the object could be in an inconsistent state and cause undefined behavior.

Furthermore, the compiler has no guarantees that undo_something will actually put self back in the exact same state it was left in; with an &mut reference, you can do all kinds of things, including deallocating internal storage and reallocating it in a different place, in which case references into the original storage would be invalid and could cause undefined behavior.

Cell (which works for Copy types, and has no runtime cost) and RefCell (which is more general, but has run-time borrow checking overhead) in std::cell, or Mutex or RwLock in std::sync, are necessary to add interior mutability to a type, in which you can mutate it through an & pointer. They all avoid any undefined behavior that could occur by having multiple &mut pointers to the same data, but all but Cell add a runtime cost (with Mutex and RwLock being more expensive than RefCell, as they provide cross-thread synchronization, while RefCell does dynamic borrowing but only within a single thread).


#5

Thank you, DanielKeep-san, lambda-san!

I will study Cell and RefCell, and and decided which I use, mut or Cell/RefCell.