1

It's a little bit tricky trying to phrase this question.

So let's say I have a large number of functions that all have varying numbers of arguments - some have none, some have one, some have a few or many more. I receive the parameters for these functions in a vector. Normally, I can just call them like this:

 #include <vector>

 int my_functionA(std::string a) { /*...*/ return 1; }     
 int my_functionB(std::string a, std::string b) { /*...*/ return 2; }  

 void some_useful_function(std::vector<std::string> vec, int choice) {
      if (choice == 1) {
           // Error checking would happen here
           int val = my_functionA(vec[0]);
           // Do something with val
           // ...
      }
      else if (choice == 2) {
           // Error checking would happen here
           int val = my_functionB(vec[0], vec[1]);
           // Do something with val
           // ...
      }
 }

Error checking would be making sure there are the correct number of arguments, etc. But this gets very tedious if I have a large number of functions, and the error checks and what I do with the return value are mostly the same (ie checking that the vector size matches the number of arguments). I end up repeating very similar ~15 lines of code 100 times, and if I ever decide I want to change or add something to that 15 line 'sequence', I'd have to redo it a hundred times.

It would make more sense if I could make a map from a choice to a data structure that has the function pointer and other information I'll need, and then the my_useful_function would be more like:

 struct funcPointer {
      funcPointer(void * f, int n) : fnc(f), numOfArgs(n) {};
      void * fnc;
      int numOfArgs;
 };

 std::map<int, funcPointer> = { 
      {1, funcPointer(my_functionA, 1)},
      {2, funcPointer(my_functionB, 2)}};

 void some_useful_function(std::vector<std::string> vec, int choice) {
      if (map.count(choice) > 0) {
           // Do stuff if map[choice].numOfArgs doesn't match vec size
           int val = map[choice].fnc(vectorSomehowConvertedToArguments);
           // Do stuff with the return value
      }
 }

This answer with the 'index trick' got me really close, but since it requires a constant for the unpack_caller, I'm not sure how to mesh that into my map / data struct.

John Laughlin
  • 57
  • 1
  • 9
  • 1
    C++11 or C++14? – Quentin Mar 21 '19 at 13:45
  • C++14 preferably. I'll edit since C++11 is probably redundant. – John Laughlin Mar 21 '19 at 13:47
  • 2
    It's not perfectly clear why the error checking can't happen outside the `if`/`else` chain if it's always the same. It's not like the value of `choice` is any more known inside than outside. Similarly, why can't you just store the return value and do the same thing with it? Further, why can't both these things be done in a wrapper with `some_useful_function` doing nothing but the function selection? – Max Langhof Mar 21 '19 at 14:00
  • The only benefit is that the error check relies on some info about the function - ie, the varying number of parameters in these example. Same for the return. So for funcA I might check if it has 1 parameter and if its return value is X, funcB I might check if it has 2 parameters and its return value is Y. That much I could easily do with a map, but it'd be cleaner to get it all done in the same map. – John Laughlin Mar 21 '19 at 14:16

1 Answers1

5

First, here's funcPointer which returns a lambda doing the std::vector-to-arguments juggling before calling a given function, generated based on the function's arity:

template <class F, std::size_t... ParamsIdx>
auto funcPointer(F f, std::index_sequence<ParamsIdx...>) {
    return [f](std::vector<std::string> const &args) {
        assert(args.size() == sizeof...(ParamsIdx));
        return f(args[ParamsIdx]...);
    };
}

template <class... Params>
auto funcPointer(int (*f)(Params...)) {
    return funcPointer(f, std::index_sequence_for<Params...>{});
}

These lambdas can then be stored together in a std::map<int, std::function<int(std::vector<std::string> const &)>>:

std::map<int, std::function<int(std::vector<std::string> const &)>> map = {
    {1, funcPointer(my_functionA)},
    {2, funcPointer(my_functionB)}
};

Finally, the call is straightforward:

void some_useful_function(std::vector<std::string> vec, int choice) {
    if (map.count(choice) > 0) {
        int val = map[choice](vec);
        // Do stuff with the return value
        std::cout << "Call succeeded, got " << val << '\n';
    }
}

See it live on Wandbox

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • That's pretty phenomenal and looks like it works perfectly. You're awesome! – John Laughlin Mar 21 '19 at 14:24
  • Follow up question that's probably pointing more toward my C++ rougher edges, is there a clean way for generic return types? Say my_funcA returns an int, my_funcB returns a string, and my_funcN may return void. – John Laughlin Mar 21 '19 at 16:49
  • 1
    @JohnLaughlin you could use a `std::variant` or `std::any` return type depending on whether your possible return types are known, but everything that depends on the actual return type will have to be located before the type-erasure step (i.e. done inside the lambda). – Quentin Mar 21 '19 at 16:54
  • @Quentin is it possible to generalize the solution for a case when the functions differ by argument types? E.g. `my_functionA(int)` and `my_functionB(std::string, std::string)`. – Alexandr Zarubkin Oct 20 '22 at 10:16