How does closure capture the context?

In cases where we update &mut self through closure inline how does it capture the context ? Below, is simple version of what I am trying to do

struct Pro1 {
    x: u32,
    y: u32
}

struct Pro2 {
    a: u32,
    b: u32,
}

struct Pro {
    z: Pro1,
    p: u32,
    l: Pro2
}

fn main() {
    let mut x = Pro {
        z: Pro1 { x: 1, y: 2},
        p: 3,
        l: Pro2 { a: 2, b: 10 },
    };

    x.prod();
}

impl Pro {
    fn prod(&mut self) {
        println!("Value = {}", self.p);
        self.l.prod2(&mut || {
            self.p = 10;
        });
        println!("Value = {}", self.p);
    }
}

impl Pro2 {
    fn prod2<F: FnMut()>(&mut self, f: &mut F) {
        self.a = 2;
        f();
    }
}

I am wondering how does it capture the context, from quick look at the assembly it seems like it allocates a new struct(?) in the stack. Is that always the case or can it allocate something in the heap as well ?

Closures capture references or move values. Capturing never allocates new memory; it won't implicitly move values into the heap.

1 Like

Closures never allocate on the heap implicitly. The only instance of “implicit” heap allocations around closures that I’m aware of is that a captured value of types like Box<T> or Vec<T> that’s captured by-value becomes part of the closure (no new heap allocation here yet) and might be cloned (this is where a new heap allocation can happen) if the whole closure is cloned (this operation itself is rather “explicit” though). It’s not the most well-known feature but closures do implement Clone if all captures support it.

Explicitly moving a whole closure to the heap of course is also possible, but you’d need to pass it to Box::new or Arc::new or the like manually for that. Doing this can become necessary to pass get proper type erasure for owned closures, resulting in types such as Box<dyn FnOnce(…) -> …> or the like.

1 Like

Basically, a closure defines an anonymous struct with a field for each capture.

let x = 5;
let my_fn = |y| x + y;

// my_fn is basically
struct MyFnClosure<'a> {
  value: &'a i32
}

impl Fn(i32)-> i32 for MyFnClosure {
    fn call(&self, y: i32) -> i32 {
        self.value + y
    }
}

and like any struct, it is local on the stack by default

5 Likes

Note that your code would more typically be written without the &mut indirection to the closure, i.e.

impl Pro {
    fn prod(&mut self) {
        println!("Value = {}", self.p);
        self.l.prod2(|| {
            self.p = 10;
        });
        println!("Value = {}", self.p);
    }
}

impl Pro2 {
    fn prod2<F: FnMut()>(&mut self, mut f: F) {
        self.a = 2;
        f();
    }
}

Usage of &mut F, F: FnMut(…) function arguments is sometimes useful, too, but as far as I’m aware, mostly the need comes up only for cases where you need to use the closure recursively.


If you think of the closure F like a struct that contains fields (in this case a single field) for the variable captures, so that’s a &mut u32 reference to self.p in this case, your case of &mut F is essentially passing a &mut &mut u32 at run-time, which is one more level of indirection than necessary.


Also note that it didn’t always use to be the case that mutating self.p in a closure would capture only self.p by mutable reference (i.e. &mut self.p). Before edition 2021, this would have captured the whole of self, (i.e. the borrow &mut self), which would have made your program not compile. (As easily seen if you switch the playground to edition 2018.)

2 Likes

Yep. Fn / FnMut / FnOnce are just traits, not types, and for each part of your source code where you write a closure expression it's essentially creating a hidden unnamed struct-like type that implements that trait and includes whatever captured contexts in its fields.

It's almost solely just syntactic sugar. If closures did not exist the exact same pattern could be implemented manually.

So when you pass it to fn prod2<F: FnMut()>(&mut self, f: &mut F) it's working by monomorphization, you're passing it in-place and prod2 calling it doesn't need any dynamic dispatch.

If there is not actually any captured state, it becomes a zero-sized type, and passing it to prod2 wouldn't even have to move around any data at all.

However, like other object-safe traits in general, if you want to put it on the heap and adding dynamic dispatch, you can Box::new(f) to create a Box<F>, which then is convertible to Box<dyn FnMut()>.

// ===== with closures ====

fn call<F: FnOnce()>(f: F) {
    f();
}

fn print_n(n: i32) {
    call(|| println!("{}", n));
}

// ==== basically an exactly equivalent program but implemented manually ====

trait MyFnOnceTrait {
    fn do_the_thing(self);
}

fn call<F: MyFnOnceTrait>(f: F) {
    f.do_the_thing();
}

fn print_n(n: i32) {
    struct MyFnOnceTraitImplementation { n: i32 }
    impl MyFnOnceTrait for MyFnOnceTraitImplementation {
        fn do_the_thing(self) {
            println!("{}", self.n);
        }
    }
    call(MyFnOnceTraitImplementation { n });
}
1 Like

You might be interested in Closures: Magic Functions | Rusty Yato

1 Like