Bashir Abdelwahed
Bashir Abdelwahed

Reputation: 512

rust closures definition inside a for loop

I faced the same problem as mentioned in this question. In short his problem is borrowing an object as mutable, due to its usage inside a closure, and borrowing it as immutable due to usage inside of a function (or macro in this case).

fn main() {
    let mut count = 0;

    let mut inc = || {
        count += 2;
    };

    for _index in 1..5 {
        inc();
        println!("{}", count);
    }
}

One solution to this problem is defining the closure inside the for loop instead of outside the for loop, or avoid capture of a variable by passing the mutable reference using the parameters of the closure:

1.

  fn main() {
      let mut count = 0;

      for _index in 1..5 {
          let mut inc = || {
              count += 2;
          };
          inc();
          println!("{}", count);
      }
  }
  fn main() {
      let mut count = 0;
    
      let inc = | count: &mut i32| {
          *count += 2;
      }; 
    
      for _index in 1..5 {
          inc(&mut count);
          println!("{}", count);
      }
  }

So I have the following questions on my mind:

  1. Which one of these follows the best practice solutions?
  2. Is there a 3rd way of doing things the right way?
  3. According to my un understanding, closures are just anonymous functions, so defining them multiple times is as efficient as defining them a single time. But I am not able to find a definite answer to this question on the official rust references. Help!

Upvotes: 2

Views: 858

Answers (1)

Emoun
Emoun

Reputation: 2507

Regarding which one is the right solutions, I would say it depends on the use case. They are so similar it shouldn't matter in most cases unless there is something else to sway the decision. I don't know of any third solution.

However, closures are not just anonymous functions but also anonymous structs: A closures is an anonymous struct that calls an anonymous function. The members of the struct are the references to borrows values. This is important because structs need to be initialized and potentially moved around, unlike functions. This means the more values your closure borrows, the more expensive it is to initialize and pass as an argument to functions (by value). Likewise, if you initialize your closure inside a loop, the initialization might happen every iteration (if it is not optimized out of the loop), making it less performant than initializing it outside the loop.

We can try and desugar the first example into the following code:

struct IncClusureStruct<'a> {
    count: &'a mut i32,
}
fn inc_closure_fn<'a>(borrows: &mut IncClusureStruct<'a>) {
    *borrows.count += 2
}

fn main() {
    let mut count = 0;

    for _index in 1..5 {
        let mut inc_struct = IncClusureStruct { count: &mut count };
        inc_closure_fn(&mut inc_struct);
        println!("{}", count);
    }
}

Note: The compiler doesn't necessarily do exactly like this, but it is a useful approximation.

Here you can see the closure struct IncClusureStructand its function inc_closure_fn, which together are used to provide the functionality of inc. You can see we initialize the struct in the loop and then call it immediately. If we were to desugar the second example, IncClusureStruct would have no members, but inc_closure_fn would take an additional argument that references the counter. The counter reference would then go to the function call instead of the struct initializer.

These two examples end up being the same efficiency-wise because the number of actual values passed to the function is the same in both cases: 1 reference. Initialize a struct with one member is the same as simply initializing the member itself, the wrapping struct is gone by the time you reach machine code. I tried this on Godbolt and as far as I can tell, the resulting assembly is the same. However, optimizations don't catch all situations. So, if performance is important, benchmarking is the way to go.

Upvotes: 1

Related Questions