I want to have some code that can take any callable object, and I don't want to expose the implementation in a header file.
I do not want to risk memory allocation on the heap or free store (risk of throwing, and performance hit, or I'm in code with no access to the heap).
Not having value semantics is probably good enough: The call with complete before the end of the current scope usually. But value semantics might be useful if not too expensive.
What can I do?
Existing solutions have issues. std::function
allocates and has value semantics, and a raw function pointer lacks the ability to transmit state. Passing a C style function pointer-void pointer pair is a pain for the caller. And if I do want value semantics, the C-style function pointer doesn't really work.
We can use type erasure without allocation by doing C-style vtables.
First, the vtable details in a private namespace:
The template iteslf. It is called
call_view<Sig>
, similar tostd::function<Sig>
:In this case, the
vtable
is a bit redundant; a structure containing nothing but a pointer to a single function. When we have more than one operation we are erasing this is wise; in this case we do not.We can replace the
vtable
with that one operation. Half of the above vtable work above can be removed, and the implementation is simpler:and it still works.
With a bit of refactoring, we can split the dispatch table (or functions) from the storage (ownership or not), to split the value/reference semantics of the type erasure from the operations type erased.
As an example, a move-only owning callable should reuse almost all of the above code. The fact that the data being type erased exists in a smart pointer, a
void const volatile*
, or in astd::aligned_storage
can be separated from what operations you have on the object being type erased.If you need value semantics, you can extend the type erasure as follows:
where we create a bounded buffer of memory to store the object in. This version only supports move semantics; the recipie to extend to copy semantics should be obvious.
This has an advantage over
std::function
in that you get hard compiler errors if you didn't have enough space to store the object in question. And as a non-allocating type, you can afford to use it within performance critical code without risking allocation delays.Test code:
Live example with all 3 tested.