I have a table of functions, all of them returning a double but with an arbitrary number of doubles as arguments. For example:
double sum1(double a, double b, double c) {
return a + b + c;
}
double sum2(double a, double b, double c, double d) {
return a + b + c + d;
}
double sum3(double a, double b) {
return a + b;
}
And I have a table that summarizes and provides some metadata for these functions
struct Function {
void* fnptr;
size_t numargs;
};
Function fntable[] = {{(void*)&sum1, 3}, {(void*)&sum2, 4}, {(void*)&sum3, 2}};
What I want to do is to be able to call say sum3
with just its index and a double*
array, and have the solution figure out to make the proper call, ie place the doubles in appropriate registers for the call.
The classic solution for this would be using a manual switch case and for each number of arguments have the call explicitly up to a maximum number say 20, perhaps with the help with macros to alleviate the typing, as shown below.
template <size_t N, typename... Args>
struct FnType {
using Call = typename FnType<N - 1, double, Args...>::Call;
};
template <typename... Args>
struct FnType<0, Args...> {
typedef double (*Call)(Args...);
};
double callfn(void* fn, const std::vector<double>& args) {
switch ( args.size() ) {
case 0: { return FnType<0>::Call(fn)(); }; break;
case 1: { return FnType<1>::Call(fn)(args[0]); }; break;
case 2: { return FnType<2>::Call(fn)(args[0],args[1]); }; break;
case 3: { return FnType<3>::Call(fn)(args[0],args[1],args[2]); }; break;
case 4: { return FnType<4>::Call(fn)(args[0],args[1],args[2],args[3]); }; break;
};
return std::numeric_limits<double>::quiet_NaN();
}
This works but I have a requirement to be able to change the maximum number of arguments arbitrarily without changing the code.
This is the driver for this feature. Everything is available on Godbolt: https://godbolt.org/z/9xGaTG491
int main(int argc, char* argv[]) {
if (argc == 1) {
std::cout << "Usage: caller <fnum> [<value>...]" << std::endl;
return 0;
}
int fnum = std::atoi(argv[1]);
int numargs = argc - 2;
std::vector<double> args(numargs);
for (int j = 0; j < numargs; ++j) args[j] = std::atof(argv[j + 2]);
Function fn = fntable[fnum];
if (fn.numargs != numargs) {
std::cout << "Wrong number of arguments for function #" << fnum << ". Got "
<< numargs << " expected " << fn.numargs << std::endl;
return 1;
}
std::cout << callfn(fn.fnptr, args) << std::endl;
}
And this is a typical session
$ ./caller 1
Wrong number of arguments for function #1. Got 0 expected 4
$ ./caller 1 1 2 3 4
10
$ ./caller 1 10 20 30 40
100
$ ./caller 0 100 200 300 400
Wrong number of arguments for function #0. Got 4 expected 3
$ ./caller 0 100 200 300
600
$ ./caller 2 4 5
9
There are a few things I am not checking as maximum function number etc but this is just a toy example.
So the question is: how to replace that switch statement with something O(1) where I can set arbitrarily the maximum number of parameters at compile time? Does metatemplate programming offer a solution?
Obviously I am not asking anyone to write my code but I would appreciate ideas.
You can use type erasure to handle any size of arguments.
class Function
{
struct BaseCaller
{
virtual ~BaseCaller() = default;
virtual size_t numargs() const = 0;
virtual double call(std::vector<double>) const = 0;
};
template <typename... Args>
requires (std::is_convertible_v<Args, double> && ...)
class Caller : public BaseCaller
{
double(*func)(Args...);
public:
explicit Caller(double(*func)(Args...)) : func(func) {}
size_t numargs() const override
{
return sizeof...(Args);
}
double call(std::vector<double> args) const override
{
return [&]<size_t... Is>(std::index_sequence<Is...>)
{
return func(args[Is]...);
}(std::index_sequence_for<Args...>{});
}
};
std::unique_ptr<BaseCaller> caller;
public:
template <typename... Args>
requires (std::is_convertible_v<Args, double> && ...)
Function(double(*func)(Args...)) : caller(std::make_unique<Caller<Args...>>(func)) {}
size_t numargs() const
{
return caller->numargs();
}
double operator()(std::vector<double> args) const
{
return caller->call(std::move(args));
}
};
You can construct them at runtime thusly:
constexpr size_t MaxArgs = 100;
template <size_t>
using double_t = double;
template <size_t... Is>
using RawFunction = double(*)(double_t<Is>...);
Function fromVoid(void * f, size_t numargs)
{
static auto cast = []<size_t... Js>(std::index_sequence<Js...>, void * f){ return Function(reinterpret_cast<RawFunction<Js...>>(f)); };
static auto array = [=]<size_t... Is>(std::index_sequence<Is...>) -> std::array<std::function<Function(void*)>, MaxArgs>
{
return { [=](void * f){ return cast(std::make_index_sequence<Is>{}, f); }... };
}(std::make_index_sequence<MaxArgs>{});
return array[numargs](f);
}