0

I would like to build a generic event-bus in c++, which allows messages of arbitrary type to be passed into it, and which allows registering of handlers based on the type of the message. IE I would like to expose a method like the following:

void registerHandler((OutputType) handler(EventType));

such that after calling this, when future events of type EventType are placed into the bus, it will be able to locate the appropriate handler and run it.

Of course I could handle this by creating some wrapper type ie:

struct EventWrapper {
    void* event;

    // Denote the type of event this is: we can register and find handlers by the type id.
    int event_type;
}

But I am hoping to avoid this, since it necessitates listing and maintaining event_types somewhere. I come from a primarily java background, and c++ templating mystifies me a little bit, so I am struggling to determine whether or not this is possible, and if so how I would go about it.

I'm perfectly willing to have a bunch of messy template magic inside the bus itself, if it allows me to provide the simplified interface I am hoping for externally.

Apologies if this is a duplicate question: it feels like the sort of thing that may be, but I am relatively new to c++ and suspect I do not know the name of the thing I want. I have tried searching for answers without luck. For the same reason I suspect my question title is not ideal :(

Edit: Occurs to me it may be worth mentioning, C++17, and would prefer not to take a dependency to accomplish this but can if it is mandatory / super complicated otherwise.

htuy42
  • 307
  • 3
  • 10
  • 1
    what does OutputType do? Do you want only one handler per EventType, or many? – Yakk - Adam Nevraumont Mar 14 '21 at 20:44
  • One handler per event type is sufficient. OutputType can be opaque / a void* / whatever, I am less concerned about that. (in practice it is frequently going to be put back into the bus). My expectation is that this shouldn't matter but if it is going to cause some problem I can also just drop it entirely and have the handler return void, the concern is primarily just EventType – htuy42 Mar 14 '21 at 21:16
  • 1
    You might be looking for [`std::type_index`](https://en.cppreference.com/w/cpp/types/type_index). It's essentially `event_type` you describe, except the compiler maintains it for you. You can use that as a key into the table of event handlers. Capture it at the point where the event is added to the bus, and have it travel together with the `void*` pointer. – Igor Tandetnik Mar 14 '21 at 23:45
  • That does look like it might work. I'll try it out, thank you! – htuy42 Mar 15 '21 at 01:25
  • 1
    What about something like this: https://stackoverflow.com/questions/60567493/generic-c-callback-map-is-there-any-better-way or https://stackoverflow.com/questions/2136998/using-a-stl-map-of-function-pointers/33837343#33837343 – Jerry Jeremiah Mar 15 '21 at 01:32

1 Answers1

1

Extrapolating from the similar answers I put in the comments, this works for me:

#include <iostream>
#include <unordered_map>
#include <typeindex>

struct OutputType {};

struct EventType1 {};
struct EventType2 {};
struct EventType3 {};

OutputType event1(EventType1 event) { std::cout << "Called: " << __PRETTY_FUNCTION__ << "\n"; }
OutputType event2(EventType2 event) { std::cout << "Called: " << __PRETTY_FUNCTION__ << "\n"; }

// we store the function pointers as OutputType(*)()
// but the type_index lookup returns a function pointer
// belonging to EventType so it is safe to cast it back
// to the real pointer type

std::unordered_map<std::type_index,OutputType(*)()> handlers;

template<typename EventType>
void registerHandler(OutputType (*handler)(EventType))
{
    handlers.insert({std::type_index(typeid(EventType)),(OutputType(*)()) handler});
}

template<typename EventType>
OutputType callHandler(EventType event)
{
    // we can use find and then check if it was found
    // or we can use at and let it throw std::out_of_range if it is not found
    
    auto it = handlers.find(std::type_index(typeid(EventType)));
    if (it == handlers.end())
        return {}; // need to do something with this
        
    return ((OutputType(*)(EventType)) it->second)(event);
}

int main()
{
    registerHandler(event1);
    registerHandler(event2);
    
    EventType1 event1; // will be found
    callHandler(event1);

    EventType2 event2; // will be found
    callHandler(event2);

    EventType3 event3; // won't be found
    callHandler(event3);

    return 0;
}

Try it at https://onlinegdb.com/H1nCABh7O

The only thing I couldn't do better is that: a cast is necessary to convert the type of the function pointer so that the map doesn't need to know the type.

I tried to fix that by having the map take a polymorphic base class pointer to a templated derived class but there is no way to make a virtual operator() that takes a templated EventType parameter so a cast to the derived class is still needed anyway...

Jerry Jeremiah
  • 9,045
  • 2
  • 23
  • 32
  • Thank you, this covers my needs. Its replacing something similar from where there also ultimately had to be a cast, so no problem there. Much appreciated! – htuy42 Mar 15 '21 at 04:11