c++templatesdynamic-dispatch

Dynamic dispatch based on Enum value


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:

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?


Solution

  • 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