I want to store several different callable objects (different types) in a container (e.g. std::array
) in order to invoke them later.
Each callable has the same return type and the same amount and type of parameters: for example, let's say all those objects can be invoked like int func(void *)
. However, they contain a varying amount of (possibly many) parameters (of only fundamental type) bound to different functions per callable type.
The question is: what is the recommended C++ (C++23) way of implementing this, while avoiding dynamic memory allocations due to the required storage for the bound function parameters in the callable?
Example implementation with std::function
:
#include <array>
#include <functional>
#include <variant>
int foo(int, void *);
int bar(char, double, void *);
int i; char c; double d;
auto callable_foo = std::function{[=](void *p){ return foo(i, p); } // binds i
auto callable_bar = std::function{[=](void *p){ return foo(c, d, p); } // binds c and d
using variant_t = std::variant<decltype(callable_foo), decltype(callable_bar)>;
std::array<variant_t, 2> container{callable_foo, callable_bar};
I am aware that in this case, a std::variant
is not even required since an array of adequate std::function
s would be okay. Nevertheless (as far as I know), std::function
s have limited storage for bound function parameters and, at some point, may require dynamic memory allocation. Same is true for lambdas (if I am not mistaken) when you store them in a container (or std::function
), even if the capture is only by value of fundamental types.
After searching and experimenting a lot, I basically found two promising solutions:
std::variant
of std::bind
s (e.g. auto callable_foo = std::bind(foo, i, _1);
instead)To summarize my questions:
std::bind
seems to be not assignable (copy and move, at least in my case). How can I put objects returned by std::bind
into a std::array<std::variant<WrapperType1, WrapperType2, ..., WrapperTypeN>>
, where each WrapperType has a data member of type decltype(std::bind(...))
?Solution (2) will clearly work, but it does not seem like the C++ way of doing this, and I hope the language allows for a cleaner way.
My favorite "I wish" solution was using a template specialization of std::function
, allowing to set the SSO storage size such that all bound parameters (size determined at compile-time) fit into the std::function
object.
I searched on google, asked ChatGPT, read several pages on cppreference, experimented with actual code, and asked colleagues.
Lambdas are not std::function
s. std::function
s store copyable and invokable objects of any type and expose those operations. To do this, they rely on possibly dynamic allocation for their storage.
std::variant
will not use dynamic allocation if its content types don't.
Lambda's won't use dynamic allocations if the capture contents don't.
auto callable_foo = [=](void *p){ return foo(i, p); // binds i
auto callable_bar = [=](void *p){ return foo(c, d, p); // binds c and d
by stripping out the std::function
we remove dynamic allocation here.
using variant_t = std::variant<decltype(callable_foo), decltype(callable_bar)>;
a downside to using a variant_t
here is that each element requires as much space as every other element, plus an index field. So if one of them is much larger than the others, you waste space. But:
std::array<variant_t, 2> container{callable_foo, callable_bar};
should work. Calling it is a bit annoying, because you need to use std::visit
.
int retval = std::visit( [&pvoid](auto& f){ return f(pvoid); }, container[0] );
you can fix that with a thin wrapper
struct callable:variant_t {
using variant_t::variant_t;
int operator()(void* pv)const {
return std::visit( [&pvoid](auto& f){ return f(pvoid); }, (variant_t const&)*this );
}
};
std::array<callable, 2> container{callable_foo, callable_bar};
again, no dynamic memory overhead (or any extra here).
You can make things a bit more efficient by combining a structure with an array of function-view types.
struct Callables {
callable_foo foo;
callable_bar bar;
// etc
};
struct helper {
void* pCallable;
int(*pfunc)(void*, void*);
int operator()(void* pv)const {
return pfunc(pCallable, pv);
}
template<class T>
helper( T* t ):
pCallable(t),
pfunc( [](void* pv, void* pv2)->int {
return (*static_cast<T*>(pv))(pv2);
} )
{}
};
Callables cbs;
std::array< helper, 2 > arr{ &cbs.foo, &cbs.bar };
Now, you can iterate over the contents of arr
and invoke each one.
Zero heap allocation, no wasted space, etc.
It does add another level of indirection.