let some_stuff: ST = ...;
let some_other_stuff: SOT = ...;
let closure = |some_argument: SA| {
use(some_stuff, some_other_stuff)
};
closure(some_argument);
is the same as:
let some_stuff: SS = ...;
let some_other_stuff: SOS = ...;
struct Closure { // defined at compile-time
some_stuff: [&_] SS,
some_other_stuff: [&_] SOS,
}
impl Closure { // defined at compile-time
fn call([&_] self, some_argument: SA) -> ...
{
use(self.some_stuff, self.some_other_stuff)
}
}
let closure = Closure { // created at runtime
some_stuff: [&_] some_stuff,
some_other_stuff: [&_] some_other_stuff,
}
closure.call(some_argument);
Ergo, creating a closure == creating a struct with the captured environment, and calling a closure == calling a method of such struct.
Thanks for the insightful response + link to @RustyYato 's blog post.
Side issue: does this mean my argument about B < A is wrong -- since there is no explicit "pointer to the raw function code" and it seems the "ref to the raw function code" is decided on the type of some Struct (which is created at compile time), so there's zero runtime over head?
Exactly, calling a closure dispatches statically to a method call (that's why each closure has a different type than any other one, even when they capture the same environment + have the same API), so there is no extra "function pointer" overhead.
The counter example, of course, is when your closure is a dyn Fn... trait object. Only then will there be a pointer to a vtable holding a pointer to the function's code / body, since because of the type erasure it is no longer possible to store the address of the code within the static information of a type, so it needs to be present at runtime.
This, by the way, is the reason why Rust's <F : Fn...> (f: F) will be even more performant than C taking two parameters: _ (*f) (void * state, ...), void * state. That signature is the one of a f: &[mut] dyn Fn...(...) -> _ argument in Rust. In other words, callbacks in C always use dynamic dispatch, whereas in Rust function genericity / templates where the address of the callback's code is hardcoded into each monomorphised function's body.
No, that would mean implicit dynamic dispatch for every closure. Instead, Rust implements each closures as a distinct (new) type, implementing some or all of the FnOnce, FnMut, and Fn traits.