Thanks for very interesting Question! I was happy to implement from scratch my own solution for it.
With regular non-templated functions, without return type and without arguments, it is trivial to wrap all functions into std::vector<std::function<void()>> cmds and call necessary command by index.
But I decided to implement a considerably more complex solution, so that is is able to handle any templated function (method) with any return type, because templates are common in C++ world. Full code at the end of my answer is complete and working, with many examples provided in main() function. Code can be copy-pasted to be used in your own projects.
To return many possible types in single function I used std::variant, this is C++17's standard class.
With my full code (at the bottom of answer) you can do following:
using MR = MethodsRunner<
Cmd<1, &A::Add3>,
Cmd<2, &A::AddXY<long long>>,
Cmd<3, &B::ToStr<std::integral_constant<int, 17>>>,
Cmd<4, &B::VoidFunc>
>;
A a;
B b;
auto result1 = MR::Run(1, a, 5); // Call cmd 1
auto result2 = MR::Run(2, a, 3, 7); // Call cmd 2
auto result3 = MR::Run(3, b); // Call cmd 3
auto result4 = MR::Run(4, b, 12, true); // Call cmd 4
auto result4_bad = MR::Run(4, b, false); // Call cmd 4, wrong arguments, exception!
auto result5 = MR::Run(5, b); // Call to unknown cmd 5, exception!
Here A and B are any classes. To MethodsRunner you provide a list of commands, consisting of command id and pointer to method. You can provide pointers to any templated method as far as you provide full signature of theirs call.
MethodsRunner when called by .Run() returns std::variant containing all possible values with different types. You can access actual value of a variant through std::get(variant), or if you don't know contained type in advance you can use std::visit(lambda, variant).
All types of usages of my MethodsRunner are shown in the main() of full code (at the end of answer).
In my class I use several tiny helper templated structs, this kind of meta-programming is very common in templated C++ world.
I used switch construct in my solution instead of std::vector<std::function<void()>>, because only switch will allow to handle a pack of arbitrary argument types and count and arbitrary return type. std::function table can be used instead of switch only in the case if all commands have same arguments types and same return value.
It is well known that all modern compilers implement switch as direct jump table if switch and case values are integers. In other words switch solution is as fast, and maybe even faster than regular std::vector<std::function<void()>> function-table approach.
My solution should be very efficient, although it seems to contain a lot of code, all the heavy-templated my code is collapsed into very tiny actual runtime code, basically there is a switch table that directly calls all methods, plus conversion to std::variant for return value, that's it, almost no overhead at all.
I expected that your command id that you use is not known at compile time, I expected that it is known only at runtime. If it is known at compile time then there is no need for switch at all, basically you can directly call given object.
Syntax of my Run method is method_runner.Run(cmd_id, object, arguments...), here you provide any command id known only at run time, then you provide any object and any arguments. If you have only single object that implements all the commands then you can use SingleObjectRunner that I implemented too in my code, like following:
SingleObjectRunner<MR, A> ar(a);
ar(1, 5); // Call cmd 1
ar(2, 3, 7); // Call cmd 2
SingleObjectRunner<MR, B> br(b);
br(3); // Call cmd 3
br(4, 12, true); // Call cmd 4
where MR is type of MethodsRunner specialized for all commands. Here single objects runners ar and br are both callable, like functions, with signature (cmd_id, args...), for example br(4, 12, true) call means cmd id is 4, args are 12, true, and b object itself was captured inside SingleObjectRunner at construction time through br(b);
See detailed console output log after the code. See also important Note after the code and log. Full code below:
Try it online!
#include <iostream>
#include <type_traits>
#include <string>
#include <any>
#include <vector>
#include <tuple>
#include <variant>
#include <iomanip>
#include <stdexcept>
#include <cxxabi.h>
template <typename T>
inline std::string TypeName() {
// use following line of code if <cxxabi.h> unavailable, and/or no demangling is needed
//return typeid(T).name();
int status = 0;
return abi::__cxa_demangle(typeid(T).name(), 0, 0, &status);
}
struct NotCallable {};
struct VoidT {};
template <size_t _Id, auto MethPtr>
struct Cmd {
static size_t constexpr Id = _Id;
template <class Obj, typename Enable = void, typename ... Args>
struct Callable : std::false_type {};
template <class Obj, typename ... Args>
struct Callable<Obj,
std::void_t<decltype(
(std::declval<Obj>().*MethPtr)(std::declval<Args>()...)
)>, Args...> : std::true_type {};
template <class Obj, typename ... Args>
static auto Call(Obj && obj, Args && ... args) {
if constexpr(Callable<Obj, void, Args...>::value) {
if constexpr(std::is_same_v<void, std::decay_t<decltype(
(obj.*MethPtr)(std::forward<Args>(args)...))>>) {
(obj.*MethPtr)(std::forward<Args>(args)...);
return VoidT{};
} else
return (obj.*MethPtr)(std::forward<Args>(args)...);
} else {
throw std::runtime_error("Calling method '" + TypeName<decltype(MethPtr)>() +
"' with wrong object type and/or wrong argument types or count and/or wrong template arguments! "
"Object type '" + TypeName<Obj>() + "', tuple of arguments types '" + TypeName<std::tuple<Args...>>() + "'.");
return NotCallable{};
}
}
};
template <typename T, typename ... Ts>
struct HasType;
template <typename T>
struct HasType<T> : std::false_type {};
template <typename T, typename X, typename ... Tail>
struct HasType<T, X, Tail...> {
static bool constexpr value = std::is_same_v<T, X> ||
HasType<T, Tail...>::value;
};
template <typename T> struct ConvVoid {
using type = T;
};
template <> struct ConvVoid<void> {
using type = VoidT;
};
template <typename V, typename ... Ts>
struct MakeVariant;
template <typename ... Vs>
struct MakeVariant<std::variant<Vs...>> {
using type = std::variant<Vs...>;
};
template <typename ... Vs, typename T, typename ... Tail>
struct MakeVariant<std::variant<Vs...>, T, Tail...> {
using type = std::conditional_t<
HasType<T, Vs...>::value,
typename MakeVariant<std::variant<Vs...>, Tail...>::type,
typename MakeVariant<std::variant<Vs...,
typename ConvVoid<std::decay_t<T>>::type>, Tail...>::type
>;
};
template <typename ... Cmds>
class MethodsRunner {
public:
using CmdsTup = std::tuple<Cmds...>;
static size_t constexpr NumCmds = std::tuple_size_v<CmdsTup>;
template <size_t I> using CmdAt = std::tuple_element_t<I, CmdsTup>;
template <size_t Id, size_t Idx = 0>
static size_t constexpr CmdIdToIdx() {
if constexpr(Idx < NumCmds) {
if constexpr(CmdAt<Idx>::Id == Id)
return Idx;
else
return CmdIdToIdx<Id, Idx + 1>();
} else
return NumCmds;
}
template <typename Obj, typename ... Args>
using RetType = typename MakeVariant<std::variant<>, decltype(
Cmds::Call(std::declval<Obj>(), std::declval<Args>()...))...>::type;
template <typename Obj, typename ... Args>
static RetType<Obj, Args...> Run(size_t cmd, Obj && obj, Args && ... args) {
#define C(Id) \
case Id: { \
if constexpr(CmdIdToIdx<Id>() < NumCmds) \
return CmdAt<CmdIdToIdx<Id>()>::Call( \
obj, std::forward<Args>(args)... \
); \
else goto out_of_range; \
}
switch (cmd) {
C( 0) C( 1) C( 2) C( 3) C( 4) C( 5) C( 6) C( 7) C( 8) C( 9)
C( 10) C( 11) C( 12) C( 13) C( 14) C( 15) C( 16) C( 17) C( 18) C( 19)
default:
goto out_of_range;
}
#undef C
out_of_range:
throw std::runtime_error("Unknown command " + std::to_string(cmd) +
"! Number of commands " + std::to_string(NumCmds));
}
};
template <typename MR, class Obj>
class SingleObjectRunner {
public:
SingleObjectRunner(Obj & obj) : obj_(obj) {}
template <typename ... Args>
auto operator () (size_t cmd, Args && ... args) {
return MR::Run(cmd, obj_, std::forward<Args>(args)...);
}
private:
Obj & obj_;
};
class A {
public:
int Add3(int x) const {
std::cout << "Add3(" << x << ")" << std::endl;
return x + 3;
}
template <typename T>
auto AddXY(int x, T y) {
std::cout << "AddXY(" << x << ", " << y << ")" << std::endl;
return x + y;
}
};
class B {
public:
template <typename V>
std::string ToStr() {
std::cout << "ToStr(" << V{}() << ")" << std::endl;
return "B_ToStr " + std::to_string(V{}());
}
void VoidFunc(int x, bool a) {
std::cout << "VoidFunc(" << x << ", " << std::boolalpha << a << ")" << std::endl;
}
};
#define SHOW_EX(code) \
try { code } catch (std::exception const & ex) { \
std::cout << "\nException: " << ex.what() << std::endl; }
int main() {
try {
using MR = MethodsRunner<
Cmd<1, &A::Add3>,
Cmd<2, &A::AddXY<long long>>,
Cmd<3, &B::ToStr<std::integral_constant<int, 17>>>,
Cmd<4, &B::VoidFunc>
>;
auto VarInfo = [](auto const & var) {
std::cout
<< ", var_idx: " << var.index()
<< ", var_type: " << std::visit([](auto const & x){
return TypeName<decltype(x)>();
}, var)
<< ", var: " << TypeName<decltype(var)>()
<< std::endl;
};
A a;
B b;
{
auto var = MR::Run(1, a, 5);
std::cout << "cmd 1: var_val: " << std::get<int>(var);
VarInfo(var);
}
{
auto var = MR::Run(2, a, 3, 7);
std::cout << "cmd 2: var_val: " << std::get<long long>(var);
VarInfo(var);
}
{
auto var = MR::Run(3, b);
std::cout << "cmd 3: var_val: " << std::get<std::string>(var);
VarInfo(var);
}
{
auto var = MR::Run(4, b, 12, true);
std::cout << "cmd 4: var_val: VoidT";
std::get<VoidT>(var);
VarInfo(var);
}
std::cout << "------ Single object runs: ------" << std::endl;
SingleObjectRunner<MR, A> ar(a);
ar(1, 5);
ar(2, 3, 7);
SingleObjectRunner<MR, B> br(b);
br(3);
br(4, 12, true);
std::cout << "------ Runs with exceptions: ------" << std::endl;
SHOW_EX({
// Exception, wrong argument types
auto var = MR::Run(4, b, false);
});
SHOW_EX({
// Exception, unknown command
auto var = MR::Run(5, b);
});
return 0;
} catch (std::exception const & ex) {
std::cout << "Exception: " << ex.what() << std::endl;
return -1;
}
}
Output:
Add3(5)
cmd 1: var_val: 8, var_idx: 0, var_type: int, var: std::variant<int, NotCallable>
AddXY(3, 7)
cmd 2: var_val: 10, var_idx: 1, var_type: long long, var: std::variant<NotCallable, long long>
ToStr(17)
cmd 3: var_val: B_ToStr 17, var_idx: 1, var_type: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, var: std::variant<NotCallable, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >
VoidFunc(12, true)
cmd 4: var_val: VoidT, var_idx: 1, var_type: VoidT, var: std::variant<NotCallable, VoidT>
------ Single object runs: ------
Add3(5)
AddXY(3, 7)
ToStr(17)
VoidFunc(12, true)
------ Runs with exceptions: ------
Exception: Calling method 'void (B::*)(int, bool)' with wrong object type and/or wrong argument types or count and/or wrong template arguments! Object type 'B', tuple of arguments types 'std::tuple<bool>'.
Exception: Unknown command 5! Number of commands 4
Note: in my code I used #include <cxxabi.h> to implement TypeName<T>() function, this included header is used only for name-demangling purposes. This header is not available in MSVC compiler, and may not be available in Windows's version of CLang. In MSVC you can remove #include <cxxabi.h> and inside TypeName<T>() do no demangling by just returning return typeid(T).name();. This header is the only non-cross-compilable part of my code, you can easily remove usage of this header if needed.
switchtable to call concrete function andnothing else