Regular function references are fixed-size and can be stored in structs. Dyn Fn references can't be. Why is that? What variable-sized info is carried along with a dyn fn? Traits themselves don't have data.
Assuming you have some trait Trait
and type T
implementing it dyn Trait
obtained from T
is basically the same thing as T
, except that compiler no longer knows it is T
. This means that dyn Trait
has same size as T
, but information about size has to be stored somewhere else: that is, it will be indirectly stored inside whatever pointer you use when handling dyn Trait
, and that pointer will be fat pointer (currently limited to two pointers: pointer to actual data + pointer to metadata).
This is just as applicable for dyn Fn
as for any other Trait
. Note that implementors of dyn Fn
can have different sizes: e.g. closures store data they use, but all regular functions have unique type and are thus zero size: Playground.
All trait objects are dynamically sized. When you coerce your Adder::add1
function pointer to a dyn Fn(T) -> T
you use unsized coercion, making the compiler forget the size of Adder::add1
.
As a concrete example why this is necessary, closures—which can capture their environment and therefore vary in size—can also implement Fn(T) -> T
and be coerced to dyn Fn(T) -> T
.
Closures can have any arbitrary amount of closed-over data.
Note that if you create dyn Fn()
from a named function, not a closure, the function item type is guaranteed to be zero-sized, which means that Box<dyn Fn()>
won't actually allocate in that case. This is also usually true of closures without captures (but not guaranteed).
Trait objects are not traits though. They are types that can assume as values all the values of the types that implement the corresponding type. As such it should be clear they can have data, since those type's values have data too. Moreover the set of such types cannot be known in advance, and with that the maximum size of the trait object. Hence why trait objects are variable sized.
Hm. Rust's documentation says:
Each instance of a pointer to a trait object includes:
- a pointer to an instance of a type
T
that implementsSomeTrait
- a virtual method table, often just called a vtable, which contains, for each method of
SomeTrait
and its supertraits thatT
implements, a pointer toT
’s implementation (i.e. a function pointer).
Does that mean each instance of a dyn Fn
has its own copy of the virtual method table?
Note that if you create
dyn Fn()
from a named function, not a closure, the function item type is guaranteed to be zero-sized, which means thatBox<dyn Fn()>
won't actually allocate in that case.
Turns out it is a closure. Here it is:
As a closure, it must have associated data, but not, I think, a virtual function table, since it's not a trait.
So I have to at least Box it. How big is that closure instance?
8 bytes? 1K bytes? , If it's big, I should combine the identical instances inside an Arc.
(Use case: fixing a bug in a graphics crate. Not my code, so I don't want to make drastic changes unnecessarily. This is the actual bug:
Using strong_count
that way causes a race condition in drop
. Well-known cause of race conditions in safe Rust Think about what happens if two threads on two CPUs execute that code at the same time. Crashes about once an hour, as a multi-threaded graphics program busily draws a large, active virtual world.
The right way to do this is to refactor so that there's an Arc around a struct that contains both the reference to the destroy_fn and the struct to be de-indexed. Then the drop
gets called exactly once when the Arc
loses all its owners.
Fine. But now I have lots of identical copies of the destroy_fn
closure. May need to fix that upstream so that there's one copy inside an Arc, if the closure is big.)
No (and the documentation is not precisely worded there), a pointer to dyn Fn
contains a pointer to the vtable. Box<dyn Fn()>
is approximately equivalent to this struct:
struct BoxDynFn {
data: *const (), // we don't know what the concrete data type actually is
vtable: &'static FnVtable,
}
struct FnVtable {
/// Size of the value
size: usize,
/// Alignment of the value
align: usize,
/// Implementation of `drop_in_place()` for the value
drop: fn(*const ()),
/// The actual `Fn::call()` method implementation.
call: fn(*const ()),
}
static FN_VTABLE_FOR_MY_CLOSURE: FnVtable = FnVtable { ... };
The compiler generates approximately one static FnVtable
for each concrete type implementing Fn
(such as a closure type) that gets coerced to dyn Fn
anywhere in the code. Then, when the coercion happens at run time, the pointer to that vtable is inserted.
(It would be exactly one vtable per type, except that for compilation performance, vtables may end up duplicated across different “codegen units”, so you cannot rely on the identity of vtable pointers.)
A closure’s type implements at least one of the Fn
FnMut
FnOnce
traits, and those implementations have vtables when needed. If a vtable does not exist for that type, then dyn
cannot exist for that type.
It is as big as a struct
containing the values (or references) the closure captures would be. You can find out exactly by calling size_of_val()
on the closure before you box it, or size_of_val(&*boxed_closure)
afterward.
Another optimization is that vtables for different types can get deduplicated. So two vtable pointers being the same does not mean the types are the same, and two vtable pointers being different does not mean that the types are different.[1] (Documentation.)
even ignoring lifetimes ↩︎