You could create your own version of std::function that accepts functions of any signature using type erasure. This will require some heavy lifting though. I will provide a solution for void functions which requires C++17, because we will use std::any.
I will walk you through the steps first and then provide a full solution in code.
- First you create some
function_traits that capture the number and type of arguments of any kind of function using template meta-programming. We can "borrow" from here.
- Then you create a class
VariadicVoidFunction that has a templated call operator.
- This call operator creates a
std::vector<std::any> and passes it to the invoke method of a member of VariadicVoidFunction, which is a (resource-owning smart) pointer of type VariadicVoidFunction::Concept.
VariadicVoidFunction::Concept is an abstract base class with a virtual invoke method that accepts std::vector<std::any>.
VariadicVoidFunction::Function is a class template, where the template parameter is a function. It stores this function as member and inherits VariadicVoidFunction::Concept. It implements the invoke method. Here, we can std::any_cast the vector elements back to the expected types, which we can extract using function_traits. This allows us to call the actual function with the correct argument types.
VariadicVoidFunction gets a templated constructor accepting any kind of function F. It creates an instance of type VariadicVoidFunction::Function<F> and stores it in an owning (smart) pointer.
#include <memory>
#include <vector>
#include <any>
// function_traits and specializations are needed to get arity of any function type
template<class F>
struct function_traits;
// ... function pointer
template<class R, class... Args>
struct function_traits<R(*)(Args...)> : public function_traits<R(Args...)>
{};
// ... normal function
template<class R, class... Args>
struct function_traits<R(Args...)>
{
static constexpr std::size_t arity = sizeof...(Args);
template <std::size_t N>
struct argument
{
static_assert(N < arity, "error: invalid parameter index.");
using type = typename std::tuple_element<N,std::tuple<Args...>>::type;
};
};
// ... non-const member function
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...)> : public function_traits<R(C&,Args...)>
{};
// ... const member function
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...) const> : public function_traits<R(C const&,Args...)>
{};
// ... functor (no overloads allowed)
template<class F>
struct function_traits
{
private:
using call_type = function_traits<decltype(&F::operator())>;
public:
static constexpr std::size_t arity = call_type::arity - 1;
template <std::size_t N>
struct argument
{
static_assert(N < arity, "error: invalid parameter index.");
using type = typename call_type::template argument<N+1>::type;
};
};
template<class F>
struct function_traits<F&> : public function_traits<F>
{};
template<class F>
struct function_traits<F&&> : public function_traits<F>
{};
// type erased void function taking any number of arguments
class VariadicVoidFunction
{
public:
template <typename F>
VariadicVoidFunction(F const& f)
: type_erased_function{std::make_shared<Function<F>>(f)} {}
template <typename... Args>
void operator()(Args&&... args){
return type_erased_function->invoke(std::vector<std::any>({args...}));
}
private:
struct Concept {
virtual ~Concept(){}
virtual void invoke(std::vector<std::any> const& args) = 0;
};
template <typename F>
class Function : public Concept
{
public:
Function(F const& f) : func{f} {}
void invoke(std::vector<std::any> const& args) override final
{
return invoke_impl(
args,
std::make_index_sequence<function_traits<F>::arity>()
);
}
private:
template <size_t... I>
void invoke_impl(std::vector<std::any> const& args, std::index_sequence<I...>)
{
return func(std::any_cast<typename function_traits<F>::template argument<I>::type>(args[I])...);
}
F func;
};
std::shared_ptr<Concept> type_erased_function;
};
You can use it like this:
#include <unordered_map>
#include <iostream>
int main()
{
VariadicVoidFunction([](){});
std::unordered_map<size_t, VariadicVoidFunction> map =
{
{0, VariadicVoidFunction{[](){ std::cout << "no argument\n"; }} },
{1, VariadicVoidFunction{[](int i){ std::cout << "one argument\n"; }} },
{2, VariadicVoidFunction{[](double j, const char* x){ std::cout<< "two arguments\n"; }} }
};
map.at(0)();
map.at(1)(42);
map.at(2)(1.23, "Hello World");
return 0;
}
no argument
one argument
two arguments
Demo on Godbolt Compiler explorer
Note that this is a prototypical solution to get you started. One downside is that all arguments will be copied into the std::any You could avoid this by passing pointers to std::any, but you have to be careful with lifetimes when you do this.