Hi there I am facing the following problem: I have a class with many member functions of a certain pattern. Each of these member functions has 3 overloads and these overloads are the same for all member functions in consideration. By "same" I mean they have the same signature except for the function name. This leads to much code repetition in control states selecting the desired method. Which is nasty to maintain when new methods are added. Moreover, I have a enum class (based on unsigned ints) with an item for each of these member functions. What I try is to create a mapping from the enum class to the member functions (See example below).
I have several member functions memfunc1
, memfunc2
, ... and I have 3 functions (say caller1
, caller2
, caller3
) each taking the arguments of one of the three overloads plus an enum as argument which tells me which of the memfuncs to call. I want to avoid doing the mapping enum to memfunc three times instead of once. I can imagine this is possible since the compiler knows signatures because the overloads of each memfunc appear in separated control paths.
enum class Algos : unsigned int {
FUN1 = 0,
FUN2,
FUN3,
NUM_OF_FUNCTIONS
};
class Algorithms {
Type1 memfunc1();
Type1 memfunc1(const Type2&) const;
Type1 memfunc1(Type3&, const Type2&);
Type1 memfunc2();
Type1 memfunc2(const Type2&) const;
Type1 memfunc2(Type3&, const Type2&);
Type1 memfunc3();
Type1 memfunc3(const Type2&) const;
Type1 memfunc3(Type3&, const Type2&);
};
What I currently do is
Type1 Algorithms::caller1(Algos algo) {
if (algo == Algos::FUN1) {
return memfunc1();
}
if (algo == Algos::FUN2) {
return memfunc2();
}
if (algo == Algos::FUN3) {
return memfunc3();
}
}
Type1 Algorithms::caller2(const Type2& arg, Algos algo) {
if (algo == Algos::FUN1) {
return memfunc1(arg);
}
if (algo == Algos::FUN2) {
return memfunc2(arg);
}
if (algo == Algos::FUN3) {
return memfunc3(arg);
}
}
Type1 Algorithms::caller3(Type3& arg1, const Type2& arg2, Algos algo) {
if (algo == Algos::FUN1) {
return memfunc1(arg1, arg2);
}
if (algo == Algos::FUN2) {
return memfunc2(arg1, arg2);
}
if (algo == Algos::FUN3) {
return memfunc3(arg1, arg2);
}
}
But I want to separate the mapping from the enums to the member function from the specific overload. Because yet all three caller functions do more or less the same:
FUN1 -> memfunc1
FUN2 -> memfunc2
FUN3 -> memfunc3
...
in way which allows to call like:
Type1 a = std::invoke(mapping(FUN1), const Type2&);
I'm not tied to exactly this syntax using std::invoke
and it's also totally fine to provide the instance of Algorithms
when calling, but I want the compiler to select the correct overload of the function based on the provided arguments. Ideally the mapping can be defined at compile time, since the enums and the provided arguments are known at compile time.
So far I read some related stack overflow posts including:
as well as many cppreference pages including https://en.cppreference.com/w/cpp/utility/functional/mem_fn
My approach so far was creating an array
using Overload2 = std::function<Type1(const Type2&)>;
std::array<Overload2, std::to_underlying(Algos::NUM_OF_FUNCTIONS)> pMethods;
and then populating the array by calling
pMethods.at(std::to_underlying(Algos::FUN1)) = [this](const Type2& var) {
return memfunc1(var);
};
and similar for other member functions and overloads. However, this is not a desired solution since it moves the code repetition to another part of the code, but does not resolves it.
They idea of using a std::unordered_map
I discarded early in the process, because as far as I know this container does not work well with the constexpr
keyword for it is non-literal type. I think I remember several SO-posts out there tackling this problem.
Using recent versions of c++ is fine for me as long as they compile in g++-14 and clang++-18. I'd prefer solutions using no libraries other than stl and prefer memory safe stl solutions over c-style ones.
Thanks in Advance!
In the moment I am too new on the platform to be allowed to upvote your answers, but I consider them all as helpful. Not only the one I'll mark as accepted.
You can have a single array for each caller member, in a template function.
class Algorithms {
Type1 memfunc1() { return 1; }
Type1 memfunc1(const Type2&) { return 1; }
Type1 memfunc1(Type3&, const Type2&) { return 1; }
Type1 memfunc2() { return 2; }
Type1 memfunc2(const Type2&) { return 2; }
Type1 memfunc2(Type3&, const Type2&) { return 2; }
Type1 memfunc3() { return 3; }
Type1 memfunc3(const Type2&) { return 3; }
Type1 memfunc3(Type3&, const Type2&) { return 3; }
template<typename... Args>
Type1 caller(Algos algo, Args... args) {
static constexpr std::array<Type1(Algorithms::*)(Args...), std::to_underlying(Algos::NUM_OF_FUNCTIONS)> dispatch = { &Algorithms::memfunc1, &Algorithms::memfunc2, &Algorithms::memfunc3 };
return std::invoke(dispatch[std::to_underlying(algo)], this, args...);
}
public:
Type1 caller1(Algos algo) {
return caller<>(algo);
}
Type1 caller2(const Type2& arg, Algos algo) {
return caller<const Type2&>(algo, arg);
}
Type1 caller3(Type3& arg1, const Type2& arg2, Algos algo) {
return caller<Type3&, const Type2&>(algo, arg1, arg2);
}
};