How to use a struct that consumes itself in a closure

I encountered a problem in the project, simplified as follows

struct OtherLib {
   a: usize,
}
impl OtherLib {
   fn new()-> Self {
       Self { a: 0 }
   }
   fn foo(self)-> Self {
       self
   }
}

fn main() {
   let mut bar = OtherLib::new();
   
   //compile error
   (0..10).for_each(|_| 
         bar = bar.foo();
   );

   //compile successfully
   //for _ in 0..10 {
   //     bar = bar.foo();
   //}
   
}

Here is playground
then

  1. why does not the struct that consumes itself work in a closure? but works in loop?
  2. If I need to use this kind of struct in closure, how can I do?

Any help would be appreciate,thanks

The problem here is that for_each requires a closure to be callable multiple times (i.e. FnMut) but such a closure may not move the value out of bar. The reason for this is that if the call to .foo() panics, then the value in bar would be gone, but the closure could still be called another time [after the caller of the closure catches the panic]. What’s supposed to happen in such a case? The compiler is conservative here and disallows your code even though for_each for Range does in-fact not catch any panics.

The reason that the for loop does work is that the compiler knows that the loop body will never run another time after it panics.

Possible solutions:

  • There’s one approach that always works if you want to move something out of a value temporarily (and move it back in afterwards), but the compiler is unhappy with it: Use Option and .take(). (This turns the problematic scenario I explained above into a runtime error. This can result in additional overhead from runtime checks [i.e. the call to unwrap] if those aren’t optimized away.)
    fn main() {
        let mut bar = Some(OtherLib::new());
        (0..10).for_each(|_| {
            bar = Some(bar.take().unwrap().foo());
        })
    }
    
  • In this case where you’re passing the closure to for_each, switch to using fold which allows you to pass owned state:
    fn main() {
        let mut bar = OtherLib::new();
        bar = (0..10).fold(bar, |bar, _| {
            bar.foo()
        });
    }
    
5 Likes

Cool, thank you very much for clear explanation.
Rust compiler is so subtle indeed that can take care so many the implicit dangerous cases.

I have another curious problem, how Rust compiler can take care so many cases? Is there a relative general way such as owership mechanisim? Like this case, need compiler to think about the specific context, or just hit the general rule of compiler?

This all boils down to ownership and borrowing, too.

Any callable thing in Rust (i.e. closures, functions and function pointers) is described by three implicitly implemented traits:

  • FnOnce, which provides method call_once. This method takes self, i.e., if the callable object is not Copy, it will be consumed after the call. This trait is implemented by every closure, since, well, every closure can be called.
  • FnMut, which provides method call_mut. This method takes &mut self, i.e. the callable object is locked exclusively for the duration of the call and released afterwards, and so can be called multiple times in succession. This trait is implemented by closures, if and only if they do not move out of their environment.
  • Fn, which provides method call. This method takes &self, i.e. the callable object is locked immutably for the duration of the call (and, again, can be reused after the call is finished), so it can be called multiple times in parallel. This trait is implemented by closures, if and only if they do not hold exclusive locks to their environment (that is, if they do not mutate it).
1 Like

Thanks for kind reply.
In this case, the closure is certain a FnMut, but what rules does it violate to cause the compilation errors? How does the compiler work in this case?

In the original post, it's not FnMut, it's only FnOnce - it moves the value out of its environment. The fact that this value is then moved back is irrelevant.

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.