3

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?

3
  • I am not sure I fully understand, but basically you want to output the name of the enum value when calling it, did I get that right? If so, your best approach (IMO) would be to use an array of structs which holds the name (for output) as well as the handler to be called. Your function call would then be Look up array element, output debug message, call handler. No switch/case, no templates. Commented Sep 24, 2022 at 5:50
  • Alternatively, just pass a pointer to 'the function to execute' to your logging handler. That will spare you the entire Enum. std::function would also work. Commented Sep 24, 2022 at 5:54
  • @RefugnicEternium Printing the name of the enum is just to show the idea and that the correct handler was called. In reality each of the handlers do different things (and accept the message as an arg, which isn't shown here either). And the enum is important (and external) to identify which handler to call. Commented Sep 24, 2022 at 5:56

4 Answers 4

2

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

Sign up to request clarification or add additional context in comments.

1 Comment

I really like this. If I can make the ct_map<message_types>::call<Handler>(MESSAGE_TYPE::TYPE_ZERO); invocation more concise for users, it'll be the way I go. Thanks!
1

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.

3 Comments

Thanks, I'd still need some sort of switch here though, right? For runtime dipatching? For example wrap_handler(std::bind(handler<TYPE_ONE>)); works, but would for (auto mt : { TYPE_ONE, TYPE_TWO }) { wrap_handler(std::bind(handler<mt>)); }
Actually, both of those are 'compile time dispatching'. The compiler translates the call in the loop to the appropriate template specialization, resulting in the correct function pointer. You only require a switch if you solely have the enum value to work with.
I'd prefer going with a template here, since it just allows for more flexibility: You could easily turn TYPE_ONE ect into constexpr objects and add more overloads, if desired: template<class F, class...Args> void wrap_handler(F&&f, Args&&...args) { std::printf("(before) ");std::forward<F>(f)(std::forward<Args>(args)...); std::printf(" (after)\n"); } and this would also remove the need to check for null, since F being deduced to int or std::nullptr_t would simply result in a compiler error.
1

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.

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.