Home > Blockchain >  Dynamic dispatch based on Enum value
Dynamic dispatch based on Enum value

Time:09-27

Lets say I'm trying to write multiple handlers for multiple message types.

enum MESSAGE_TYPE { TYPE_ZERO, TYPE_ONE, TYPE_TWO, TYPE_THREE, TYPE_FOUR };

One solution might be

void handler_for_type_one(...){ ... }
void handler_for_type_two(...){ ... }
...

switch(message_type){
  case TYPE_ONE: handler_for_type_one(); break;
  case TYPE_TWO: handler_for_type_two(); break;
...

And yeah, that would work fine. But now I want to add logging that wraps each of the handlers. Let's say a simple printf at the beginning / end of the handler function (before and after is fine too).

So maybe I do this:

template<MESSAGE_TYPE>
void handler() {
    std::printf("[default]");
}

template<> void handler<TYPE_ONE>() {
    std::printf("[one]");
}

template<> void handler<TYPE_TWO>() {
    std::printf("[two]");
}

template<> void handler<TYPE_THREE>() {
    std::printf("[three]");
}

int main()
{
    std::printf("== COMPILE-TIME DISPATCH ==\n");
    handler<TYPE_ZERO>();
    handler<TYPE_ONE>();
    handler<TYPE_TWO>();
    handler<TYPE_THREE>();
    handler<TYPE_FOUR>();
}

And it works how I'd expect:

== COMPILE-TIME DISPATCH ==
[default][one][two][three][default]

When the message-type is known at compile time, this works great. I don't even need that ugly switch. But outside of testing I won't know the message type and even if I did, wrap_handler (for the logging) "erases" that, requiring me to use the switch "map".

void wrap_handler(MESSAGE_TYPE mt) {
    std::printf("(before) ");
    switch (mt) {
      case TYPE_ZERO:  handler<TYPE_ZERO>();  break;
      case TYPE_ONE:   handler<TYPE_ONE>();   break;
      case TYPE_TWO:   handler<TYPE_TWO>();   break;
      case TYPE_THREE: handler<TYPE_THREE>(); break;
    //case TYPE_FOUR:  handler<TYPE_FOUR>();  break; // Showing "undefined" path
      default:         std::printf("(undefined)");
    }
    std::printf(" (after)\n");
}

int main()
{
    std::printf("== RUNTIME DISPATCH ==\n");
    wrap_handler(TYPE_ZERO);
    wrap_handler(TYPE_ONE);
    wrap_handler(TYPE_TWO);
    wrap_handler(TYPE_THREE);
    wrap_handler(TYPE_FOUR);
}
== RUNTIME DISPATCH ==
(before) [default] (after)
(before) [one] (after)
(before) [two] (after)
(before) [three] (after)
(before) (undefined) (after)

My "goals" for the solution are:

  • Have the enum value as close to the handler definition as possible -- template specialization like I show above seems to be about the best I can do in this area, but I have no idea.
  • When adding a message-type/handler, I'd prefer to keep the changes as local/tight as possible. (Basically, I'm looking for any way to get rid of that switch).
  • If I do need a switch or map, etc., since it'd be far away from the new handler, I'd like a way at compile time to tell whether there's a message type (enum value) without a corresponding switch case. (Maybe make the switch a map/array? Not sure if you can get the size of an initialized map at compile time.)
  • Minimize boilerplate

The other solution that seems obvious is a virtual method that's overridden in different subclasses, one for each message type, but it doesn't seem like there's a way to "bind" a message type (enum value) to a specific implementation as cleanly as the template specialization above.

Just to round it out, this could be done perfectly with (other languages) decorators:

@handles(MESSAGE_TYPE.TYPE_ZERO)
def handler(...):
    ...

Any ideas?

CodePudding user response:

The way I understand it, a function pointer may be what you need.

Going from your example, the code would be like this:

template<MESSAGE_TYPE>
void handler() {
    std::printf("[default]");
}

template<> void handler<TYPE_ONE>() {
    std::printf("[one]");
}

template<> void handler<TYPE_TWO>() {
    std::printf("[two]");
}

template<> void handler<TYPE_THREE>() {
    std::printf("[three]");
}

void wrap_handler(void (*handler)()) {
    std::printf("(before) ");
    if (!handler)
        std::printf("(undefined)");
    else
        handler();
    std::printf(" (after)\n");
}

int main()
{
    std::printf("== COMPILE-TIME DISPATCH ==\n");
    handler<TYPE_ZERO>();
    handler<TYPE_ONE>();
    handler<TYPE_TWO>();
    handler<TYPE_THREE>();
    handler<TYPE_FOUR>();

    std::printf("\n\n");
    std::printf("== RUNTIME DISPATCH ==\n");
    wrap_handler(TYPE_ZERO);
    wrap_handler(TYPE_ONE);
    wrap_handler(TYPE_TWO);
    wrap_handler(TYPE_THREE);
    wrap_handler(TYPE_FOUR);
}

The function pointer mirrors the prototype of the function (meaning all calls need to be compatible). In order to pass an argument, the function would change to:

void wrap_handler(void (*handler)(ArgumentType), const ArgumentType &arg) {
    std::printf("(before) ");
    if (!handler)
        std::printf("(undefined)");
    else
        handler(arg);
    std::printf(" (after)\n");
}

A way around this would be to use std::function (C 11).

void wrap_handler(std::function<> handler) {
    std::printf("(before) ");
    if (!handler)
            std::printf("(undefined)");
    else
            handler();
    std::printf(" (after)\n");
}

Possible ways to call this include:

wrap_handler(&functionWithoutArguments);
wrap_handler(std::bind(functionWithArgument, someArgument);
wrap_handler([=](){ LambdaCode; });
etc.

CodePudding user response:

This is a common problem for all applications receiving messages or events. However, in C the switch or some kind of table of handlers is the best you can do. The reason is that the value of the enum only exists at run-time, therefore you cannot make that decision at compile time. Other languages, like Python, can provide the solution you are looking for, because they are interpreted languages, so compile time and run-time are the same.

Boost asio is good example of how you can hide the switch, but my experience is that hiding it is not as good as you think at the first place. When you need to debug your code or someone else has to find the handler which belongs to a certain event, or somehow, you have to check if the handler is registered you need to know, where the switch is, place a break point there, or log the incoming messages. This is much more difficult in systems like asio.

CodePudding user response:

C requires the exact function signature to be figured out at compile time. This does include determining template parameters. You won't be able to get rid of some logic that determines the exact operation to execute, whether you're creating a map-like data structure for this or keep it a switch. If you're just worried about accidentally leaving out some enum constant in the switch or about the boilerplate code this may be the time to get the preprocessor involved.

#ifdef MESSAGE_TYPES
#    error macro name conflict for MESSAGE_TYPES may result in errors
#endif

// x is a function-like macro that takes 1 parameter (2, if you want the constants to assigned a specific value)
#define MESSAGE_TYPES(x) \
   x(TYPE_ZERO)          \
   x(TYPE_ONE)           \
   x(TYPE_TWO)           \
   x(TYPE_THREE)         \
   x(TYPE_FOUR)

#ifdef MESSAGE_TYPE_ENUM_CONSTANT
#    error macro name conflict for MESSAGE_TYPE_ENUM_CONSTANT may result in errors
#endif

#define MESSAGE_TYPE_ENUM_CONSTANT(c) c,

enum MESSAGE_TYPE { MESSAGE_TYPES(MESSAGE_TYPE_ENUM_CONSTANT) };

#undef MESSAGE_TYPE_ENUM_CONSTANT

template<MESSAGE_TYPE>
void handler() {
    std::printf("[default]");
}

template<> void handler<TYPE_ONE>() {
    std::printf("[one]");
}

template<> void handler<TYPE_TWO>() {
    std::printf("[two]");
}

template<> void handler<TYPE_THREE>() {
    std::printf("[three]");
}

void wrap_handler(MESSAGE_TYPE mt) {
    std::printf("(before) ");

#ifdef HANDLER_CALL_SWITCH_CASE
#    error macro name conflict for HANDLER_CALL_SWITCH_CASE may result in errors
#endif

#define HANDLER_CALL_SWITCH_CASE(c) case c: handler<c>(); break;

    switch (mt) {
        MESSAGE_TYPES(HANDLER_CALL_SWITCH_CASE);
        default:
            std::printf("(undefined)");
            break;
    }

#undef HANDLER_CALL_SWITCH_CASE

    std::printf(" (after)\n");
}

#undef MESSAGE_TYPES

CodePudding user response:

One way I'd get rid of the manual switch statements is to use template recursion, as follows. First, we create an integer sequence of your enum class, like so:

enum MESSAGE_TYPE { TYPE_ZERO, TYPE_ONE, TYPE_TWO, TYPE_THREE, TYPE_FOUR };

using message_types = std::integer_sequence<MESSAGE_TYPE, TYPE_ZERO, TYPE_ONE, TYPE_TWO, TYPE_THREE, TYPE_FOUR>;

Second, let's change slightly the handler and make it a class with a static function:

template <MESSAGE_TYPE M>
struct Handler
{
    // replace with this whatever your handler needs to do
    static void handle(){std::cout << (int)M  << std::endl;}
};

// specialise as required
template <>
struct Handler<MESSAGE_TYPE::TYPE_FOUR>
{
    static void handle(){std::cout << "This is my last message type" << std::endl;}
};

Now, with these we can easily use template recursion to create a generic switch map:

template <class Sequence>
struct ct_map;

// specialisation to end recusion    
template <class T, T Head>
struct ct_map<std::integer_sequence<T, Head>>
{
    template <template <T> class F>
    static void call(T t)
    {
        return F<Head>::handle();
    }
};

// recursion
template <class T, T Head, T... Tail>
struct ct_map<std::integer_sequence<T, Head, Tail...>>
{
    template <template <T> class F>
    static void call(T t)
    {
        if(t == Head) return F<Head>::handle();
        else return ct_map<std::integer_sequence<T, Tail...>>::template call<F>(t);
    }
};

And use as follows:

int main()
{
    ct_map<message_types>::call<Handler>(MESSAGE_TYPE::TYPE_ZERO);
    ct_map<message_types>::call<Handler>(MESSAGE_TYPE::TYPE_THREE);
    ct_map<message_types>::call<Handler>(MESSAGE_TYPE::TYPE_FOUR);
 }

If now, you want to create your wraphandler, you can do this:

template <MESSAGE_TYPE M>
struct WrapHandler
{
    static void handle()
    {
        std::cout << "Before" << std::endl;
        Handler<M>::handle();
        std::cout << "After" << std::endl;
    }
};

int main()
{
    ct_map<message_types>::call<WrapHandler>(MESSAGE_TYPE::TYPE_THREE);
}

Live code here

  • Related