A fn(arg) -> res
is a function pointer, i.e. a pointer to some compiled code in the original binary file.
A Box<dyn Fn(arg) -> res>
consists of three parts: One pointer to a (potentially empty) heap allocation, and two function pointers (bundled in a static vtable) that implement
- the call functionality (and has the heap data as an additional argument), as well as
- what happens when the
Box<dyn Fn(arg) -> res>
is dropped.
If your really accurate, the vtable also contains additional layout information.
A closure can capture variables, but if it does so, you’ve got to handle this extra data. fn(arg) -> res
is nothing but a simple pointer code, there’s no way to store captured variables with it. Box<dyn Fn(arg) -> res>
can store the values of the captured variables on the heap, and also properly drop them eventually when the closure is no longer needed.
It’s basically more-or-less equivalent to something like (pseudo-code)
struct Box<dyn Fn(Foo) -> Bar> {
data: *const u8, // type erased, i.e. basically a "void pointer"
// ^^^ to be more precise this would need to be a non-null pointer,
// (and a unique pointer) but I'll ignore that for simplicity
vtable: &'static VtableOf<Box<dyn Fn(Foo) -> Bar>>,
}
struct VtableOf<Box<dyn Fn(Foo) -> Bar>> {
on_drop: fn(*const u8),
layout: Layout, // size and alignment of the heap data
call: fn(*const u8, Foo) -> Bar,
// to be precise, there’d also be two more function pointers
// for `call_mut` and `call_once` (the `FnMut` and `FnOnce` methods)
// included in this vtable, but I’ll ignore this for simplicity
}
and calling the closure called, say, "f(foo)
", does
(f.vtable.call)(f.data, foo)
while dropping f
does essentially
(f.vtable.on_drop)(f.data);
if f.vtable.layout.size() > 0 {
deallocate(f.data, f.vtable.layout);
}
Now, if the closure represented in this way doesn’t actually contain any captured data, then the layout size is zero, so there’s no allocated data, dropping it won’t deallocate anything, also the on_drop
function will be a no-op, and the value of the data
pointer is completely irrelevant
This reduces the relevant parts of the Box<dyn Fn(Foo) -> Bar>
to basically just the vtable.call
function pointer (which still accepts an additional *const u8
data argument, but as I said, that pointer value is irrelevant i.e. it isn’t actually going to point anywhere, and it’s going to be ignored).
So in this case, there’s a small overhead compared to just using an fn(Foo) -> Bar
because the data
pointer itself still takes up some space, calling the closure will still have to dereference first the vtable
reference, and then the call
function pointer (that’s one more level of indirection), and dropping the thing will still call the on_drop
function pointer (even though that doesn’t do anything ) and check the size to determine that nothing needs to be deallocated.
Regarding best practices, it’s really a question of use-cases. Do you need to support capturing closures? If that’s a definite no, then fn
pointer is a good choice. Also fn(…) -> …
pointers can be copied, which can be handy in some situations. Not sure which to choose, and Box<dyn Fn(…) -> …>
works fine (because you don’t need to copy the thing)? Well, the overhead is minimal, so choosing Box<dyn Fn(…) -> …>
is okay, too. There’s even more choices with Box<dyn FnMut …>
(if you don’t need to support multiple parallel calls) of even Box<dyn FnOnce …>
if you only need to call the thing once. And then there’s auto-traits… in a multi-threaded environment, you might need Box<dyn Fn(…) -> … + Send + Sync>
. (For comparison, function pointers would already always support Send + Sync
automatically, so there’s no choices you need to make there.)