I am trying to understand the trade offs of the different ways you can pass a closure to a function. All the ways I could discover I have added in the example below. I understand how generics work (you are trading code size for speed with monomorphization). I am guessing that &dyn is passing a pair of pointers (one for the function and one for the data). But I don't know about the rest.
fn generic<T>(x: T) -> i64
where T: Fn(&str) -> i64 {
println!("generic = {}", std::mem::size_of::<T>());
x("foo")
}
fn impl_trait(x: impl Fn(&str) -> i64) -> i64 {
// doesn't work
// println!("{}", std::mem::size_of::<impl Fn(&str) -> i64>());
x("foo")
}
fn dyn_ref(x: &dyn Fn(&str) -> i64) -> i64 {
println!("dyn = {}", std::mem::size_of::<&dyn Fn(&str) -> i64>());
x("foo")
}
fn impl_ref(x: &impl Fn(&str) -> i64) -> i64 {
// doesn't work
// println!("dyn = {}", std::mem::size_of::<&impl Fn(&str) -> i64>());
x("foo")
}
fn main() {
let func = |x: &str| {
x.len() as i64
};
generic(func);
impl_trait(func);
dyn_ref(&func);
impl_ref(&func);
let outer = 9;
let other = 5;
let func = |x: &str| {
(x.len() + outer + other) as i64
};
generic(func);
impl_trait(func);
dyn_ref(&func);
impl_ref(&func);
}
This signature:
fn impl_trait(x: impl Fn(&str) -> i64) -> i64
is equivalent, codegen-wise, to this one:
fn impl_trait<F: Fn(&str) -> i64>(x: F) -> i64
So impl_trait and impl_ref both use monomorphization + static dispatch, while dyn_ref uses type erasure + dynamic dispatch.
The reason you can't call std::mem::size_of::<impl Fn(&str) -> i64>() is that impl Fn(&str) -> i64 is not a specific type, like &dyn Fn(&str) -> i64 is, but a placeholder that you can use in function signatures to either introduce an implicit generic parameter (when it's in argument position) or refer to an unnamed/unnameable concrete type (in return position).
So there are really only two ways to accept closures correct?
- monomorphization + static dispatch :
generic, impl_trait, and impl_ref use this
- type erasure + dynamic dispatch:
dyn_ref uses this
Why are there so many different syntax's for first one?
Yep.
Well, there are only two syntaxes: with an explicit generic parameter and with impl. impl_trait and impl_ref are using the same syntax, it's just that you're passing the closure by value in one case and by shared reference in the other. The explictly-generic syntax, with <F: Fn(&str) -> i64>, is the "original" one; the impl syntax was added by this RFC, which describes the motivation for it.
Pedantically a third way, closures that don't capture state coerce to function pointers.
// This will accept your `func`
fn fn_ptr(x: fn(&str) -> i64) -> i64 {
println!("{}", std::mem::size_of::<fn(&str) -> i64>());
x("foo")
}