Mutating `self` via callbacks in a method?

I can’t get the following code to work (it’s a simplified version of my actual code):

pub struct DataContainer {
    preprocessors: Vec<Box<dyn Fn(&mut DataContainer)>>,
    pub data: String,
}
impl DataContainer {
    pub fn preprocess(&mut self) {
        for p in &self.preprocessors {
            p(self);
        }
    }
    // (More methods)
}

I see the problem but I don’t see a solution.

Error message:

   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
 --> src/lib.rs:8:13
  |
7 |         for p in &self.preprocessors {
  |                  -------------------
  |                  |
  |                  immutable borrow occurs here
  |                  immutable borrow later used here
8 |             p(self);
  |             ^^^^^^^ mutable borrow occurs here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

if you're committed to this design, you could consider having a no-op placeholder callback, and swapping it into the Vec in order to call each callback.

1 Like

Alternatively, temporarily remove all preprocessors.

Maybe redesign things so you have a struct that holds the preprocessors and the other data in a sub-struct, and your preprocessors are FnMut(&mut SubStruct).

1 Like

if you're committed to this design, you could consider having a no-op placeholder callback, and swapping it into the Vec in order to call each callback.

Thanks! I don’t understand what that means. Can you elaborate?

Here's a per-preprocessor example. I think with a little imagination, you can see how this reintroduces the possibility of iterator invalidation (albeit without UB): a preprocessor could alter the list of preprocessors, so that for example this line panics because [idx] no longer exists...

noop = std::mem::replace(&mut self.preprocessors[idx], p);

...or it changes the order and you don't actually get the no-op back out and end up losing a valid preprocessor, etc etc.

The remove-all-preprocessors version I gave actually has related problems: A preprocessor could add some stuff to the list which is later thrown out; users could come to rely on the ability to populate the list and call your function recursively, which ties your hands in terms of implementation.

A substruct redesign avoids all this by removing the ability of a preprocessor to effect the list.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.