Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ template dispatch with enum variables

I have a function with a template of two enum variables. Is there any way to dispatch the function f with different combinations of enumA and enumB? I do not want to write too much if. In this case, I may only need to write 4 branches, but in actual case, I need to write tens branches.

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cassert>
using namespace std;

enum EnumA
{
    enumA0,
    enumA1
};
enum EnumB
{
    enumB0,
    enumB1
};

template <EnumA enumA, EnumB enumB>
void f()
{
    if (enumA == enumA0)
    {
        if (enumB == enumB0)
        {
            // something here
            return;
        }
        else if (enumB == enumB1)
        {
            // something here
            return;
        }
        else
        {
            assert(false);
        }
    }
    else if (enumA == enumA1)
    {
        if (enumB == enumB0)
        {
            // something here
            return;
        }
        else if (enumB == enumB1)
        {
            // something here
            return;
        }
        else
        {
            assert(false);
        }
    }
}

int main()
{
    int a, b;
    cin >> a >> b;
    const auto enumA = a < 0 ? enumA0 : enumA1;
    const auto enumB = b < 0 ? enumB0 : enumB1;
    f<enumA, enumB>(); // Is there a way to dispatch this with different enumA and enumB combinations?
    return 0;
}
like image 695
Lmxyy Avatar asked Dec 03 '25 04:12

Lmxyy


1 Answers

If your main concern is avoiding repetitive, nested if/else or switch/case constructs you could create a lookup table instead as Igor pointed out.

For this you could separate the functionality (your "something here" parts) into individual functions that will then be retrieved by your lookup table.

Thus we would remove the branching from your template function. This is now only a catch-all for undefined combinations.

template <EnumA enumA, EnumB enumB>
void f() {
    assert(false);
}

To specify what should happen depending on the combination we can specialize the function template.

template <>
void f<enumA0, enumB0>() {
    // something here
    puts("0-0");
}
template <>
void f<enumA1, enumB0>() {
    // something here
    puts("1-0");
}
template <>
void f<enumA0, enumB1>() {
    // something here
    puts("0-1");
}
template <>
void f<enumA1, enumB1>() {
    // something here
    puts("1-1");
}

Having defined what should happen when, the only gap we have to bridge is how to "convert" enum values known at runtime to the non-type template parameters (aka. enum values known at compile time) we have used in our function definitions. In other words how do we get from enum values passed to a function (e.g. dispatch(enumA1, enumB0) to the call of our function specialization (e.g. f<enumA1, enumB0>). This is where the lookup table comes into play.

void dispatch(EnumA a, EnumB b){
static const std::map<std::pair<EnumA, EnumB>, void(*)()> lookup = {
    {{enumA0, enumB0}, &f<enumA0, enumB0>},
    {{enumA0, enumB1}, &f<enumA0, enumB1>},
    {{enumA1, enumB0}, &f<enumA1, enumB0>},
    {{enumA1, enumB1}, &f<enumA1, enumB1>},
};
    return lookup.at({a, b})();
}

We store the corresponding function pointer to one of the specializations at the key for that combination. Dispatching to the right function is now a simple lookup. Now your main would look like this:

int main()
{
    int a, b;
    cin >> a >> b;
    const auto enumA = a < 0 ? enumA0 : enumA1;
    const auto enumB = b < 0 ? enumB0 : enumB1;
    dispatch(enumA, enumB);
    return 0;
}

Caveats

  • std::map is definitely not the best-suited data structure for the lookup table, however it nicely explains the idea without obfuscating it with optimizations
  • When adding an enum value you will have to provide the extra function specializations and several entries in the lookup table. This will quickly become cumbersome
  • Before going down this route it is surely recommended to take a critical second look at the overall design of the component you are writing and evaluate whether there isn't a simpler solution that avoids your stated problem in the first place by changing the design.

Bonus

It is technically possible to create the lookup table automatically which drastically reduces the amount of work needed when adding further values to the enums. It does not however solve any design flaws in the component while definitely adding complexity.

template <auto...>
struct EnumAsTypes {};
template <auto... Params>
consteval size_t Size(EnumAsTypes<Params...>) {
    return sizeof...(Params);
}
using ATypes = EnumAsTypes<enumA0, enumA1/*, enumA2*/>;
using BTypes = EnumAsTypes<enumB0, enumB1>;

void dispatch2(EnumA a, EnumB b) {
    static constinit auto lookup = []<EnumA... As, EnumB... Bs>(
                                       EnumAsTypes<As...> as,
                                       EnumAsTypes<Bs...> bs) {
        typedef void (*FunctionType)();
        std::array<FunctionType, Size(as) * Size(bs)> lookup;
        (([&]<EnumA InnerA>() {
             ((lookup[InnerA + sizeof...(As) * Bs] = f<InnerA, Bs>), ...);
         }.template operator()<As>()),
         ...);
        return lookup;
    }(ATypes{}, BTypes{});

    lookup[a + Size(ATypes{}) * b]();
}

You can play around with it on compiler explorer here

like image 58
S. Jung Avatar answered Dec 05 '25 18:12

S. Jung