0

Implementing a simple toy eventloop in C++, I bumped into a problem. I have an interface (abstract class) Event which is implemented by different types of events (keyboard, asynchronous calls, ...). In my example SomeEvent is the implementation of Event.

enum class EventType {
  SOME_EVENT = 0,
};

class Event {
public:
  virtual ~Event() {}
  virtual EventType get_event_type() = 0;
};

class SomeEvent: public Event {
public:
  SomeEvent(): m_event_type(EventType::SOME_EVENT) {}
  void bar() { std::cout << "hello" << std::endl; }
  EventType get_event_type() { return this->m_event_type; }

public:
  EventType m_event_type;
};

An EventHandler is a lambda that takes an Event, does something with it and returns nothing. I have a map of EventHandler associated to an event type. (The map points to an EventHandler in this MWE but in my original implementation it points to a vector of course)

typedef std::function<void(std::unique_ptr<Event>)> EventHandler;

std::map<EventType, EventHandler> event_handlers;

template <class T>
void add_event_handler(
  EventType event_type,
  const std::function<void(std::unique_ptr<T>)> &event_handler) {
  event_handlers[event_type] = event_handler; // Does not compile
}

When I inject an event with inject_event, I just fetch the event handler in the map and call it with the event as argument.

void inject_event(std::unique_ptr<Event> event) {
  event_handlers[event->get_event_type()](std::move(event));
}

The problem comes from the genericity of this map. It supposed to hold a handler that takes an Event but I guess there is no inheritance link between a lambda taking an interface and a lambda taking an implementation of that interface, so it fails because it can't assign a std::function<void(SomeEvent)> to a variable of type ``std::function`.

I've been scratching my head over this and my C++ is rusty AND not up to date at all. How would you go about it? Is there a pattern or a functionality in the language that would allow me to specify a lambda in a generic way? Tried using auto in EventHandler definition but it does not seem to be allowed.

Luke Skywalker
  • 1,464
  • 3
  • 17
  • 35
  • It appears that you cannot capture anything in your lambda if you want to use function pointers to access it: see https://stackoverflow.com/questions/28746744/passing-capturing-lambda-as-function-pointer – Pablo Oliva Oct 05 '19 at 12:36
  • 1
    @PabloOliva is it really my problem though? I am not using function pointer. My understanding is that std::function is not a function pointer and you can use capturing lambda. – Luke Skywalker Oct 05 '19 at 12:42

3 Answers3

2

Well, the trend in C++ is to prefer static over dynamic polymorphism (and actually, static or compile-time everything really...); and in your case - you can't have unexpected event types at run-time.

So, perhaps you can forget about most of the code in your example and try something based on std::variant:

  • Template your event-handling code on the event type.
  • Instead of EventType - just use std::variant::index() on an event to get its numeric type index among possible event types; or better yet - don't use it at all, your event-type template might be enough.
  • Instead of class Event and class SomeEvent : Event, you'll have using Event = std::variant<events::Keyboard, events::Foo, events::Some>. Maybe you'll have
    namespace events { 
        class Base { void bar() { /* ... */ } };
        class Foo : class Base { /* ... */ };
    }
    
    with all events inheriting from Base (but non-virtually).
  • an Event will not need to explicitly store or read its type - it'll always just know (unless you actively type-erase that is).
  • No more need to pass Event*'s, you can just pass Events by value, or by move-references. So no more std::unique_ptrs with mismatched classes.
  • As for your lambda, it might well become just:

    void inject_event(Event&& event) {
        std::visit(event_handlers,event);
    }
    

    with event_handler being a visitor.

Now - it's true that you can't always escape dynamic polymorphism; and sometimes you can, but the organization/company you're working at is too sluggish/unwieldy to allow for that to happen. However, if you can pull this off, it might save you a lot of headaches.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Interesting answer. It achieves what I was looking for. However the declaration of `Event` as a list of "variant" seems limited. I don't know `std::variant` yet, but it seems that you can't declare additional event types ex post with this methods right? @JeffGarett answers looks simpler to me. I wonder what are your thoughs on it. – Luke Skywalker Oct 07 '19 at 19:22
  • 1
    @LukeSkywalker: Indeed, with a variant - you're using a closed list. But as long as you only introduce new events through your _source code_, not at run-time, that's ok. – einpoklum Oct 07 '19 at 19:26
1

As alluded to in the other answer, this is an area with a lot of preexisting art. Recommended search terms for more research are "double dispatch", "multiple dispatch", and "open multi-methods".

In approaching such problems, I find it best to think about where one has information and how to transport it to where it needs to be. When adding the handler, you have the event type and when using it you need to reconstitute the event as the correct type. So the other answer's suggestion of adding extra logic around the handler is a decent approach. However, you don't want your users to do this. If you send the knowledge of the event type down one layer, you can do this wrapping yourself for your user, e.g.:

template<typename EventParameter, typename Function>
void add_event_handler(Function&& f)
{
    event_handlers[event_type<EventParameter>()] = [=](std::unique_ptr<Event> e) {
        f(event_cast<EventParameter>(std::move(e)));
    };
}

Users can use it easily, with the derived type, with no extra wrapping required on their end:

add_event_handler<SomeEvent>([](std::unique_ptr<SomeEvent>) {
    e->bar();
});

This is about as easy or as difficult as before, but instead of an enumerator, a template argument is passed so as to communicate the event type.

A very related concern is getting rid of the error prone boilerplate get_event_type. If someone specifies the wrong value, bad things happen. If you may use RTTI, you can let the compiler decide a reasonable map key:

// Key for the static type without an event
template<typename EventParameter>
auto event_type() -> std::type_index
{
    return std::type_index(typeid(EventParameter));
}

// Key for the dynamic type given an event    
template<typename EventParameter>
auto event_type(const EventParameter& event) -> std::type_index
{
    return std::type_index(typeid(event));
}

std::map<std::type_index, EventHandler> event_handlers;

It is related, because to the extent you can impose the constraint that the map key and the handler argument are the same, the more cheaply you can convert the event back to its original form. For example, in the above, a static_cast can work:

template<typename To, typename From>
std::unique_ptr<To> event_cast(std::unique_ptr<From> ptr)
{
    return static_cast<To*>(ptr.release());
}

Of course, there is a very subtle difference in this approach and using hand-rolled RTTI. This dispatches based on the dynamic type, whereas with hand-rolled RTTI, you can derive further to add extra to the event without the handler knowing it. But a mismatch between hand-rolled RTTI type and actual dynamic type can also be a mistake, so it cuts both ways.

Jeff Garrett
  • 5,863
  • 1
  • 13
  • 12
  • Perfect. My working example in what I consider an acceptable form: http://ideone.com/8EUmMW. Thanks @Jeff Garett – Luke Skywalker Oct 07 '19 at 19:16
  • The last concern I might have, from my implementation, is the need to transfer the unique_ptr for the cast in `add_event_handler`. – Luke Skywalker Oct 07 '19 at 19:24
  • Is your concern one of clarity? Or...? – Jeff Garrett Oct 07 '19 at 19:57
  • I did not like the need to cast from Event to EventType and thus recreate a unique_ptr on each event insertions. Following @einpoklum's answer, I removed the pointers and replace them with r-value references. Here is the updated code: http://ideone.com/qGZvXr – Luke Skywalker Oct 08 '19 at 05:42
  • Fair enough. I also like that better. Small note that I realized later, in insert_event, using operator[] without checking if the type is in the map can result in a bad_function_call exception if it is not (because it default-constructs a function and then calls it). – Jeff Garrett Oct 08 '19 at 14:38
0

I think your problem comes from the fact that std::unique_ptr<Event> is not compatible with std::unique_ptr<SomeEvent>. It is carried over to the std::function<...> part, as you either have a function that accept Event or SomeEvent. Technically you could invoke former with a SomeEvent but not vice versa. However you cannot insert something that handles SomeEvent to a list that handles regular Event.

What you are trying to achieve here is double dispatch based on the type of the argument. With your current setup the best you can do is to have generic Event handlers in a collection and pass the SomeEvent instances. If you want to invoke different handlers to SomeEvent instances you need double dispatching / visitor pattern. You can (inside the handler) try and dynamic cast your pointer to SomeEvent if you are absolutely sure that it is the right type. This still works best with naked pointers. E.g:

auto handler = [](Event* event){
  if (SomeEvent* someEvent = std::dynamic_cast<SomeEvent*>(event); someEvent != null){  
    // handle some event
  }
}

add_event_handler(EventType::SOME_EVENT, handler);
typedef std::function<void(Event*)> EventHandler;

std::map<EventType, EventHandler> event_handlers;

void add_event_handler(
  EventType event_type,
  const std::function<void(Event*)> &event_handler) {
  event_handlers[event_type] = event_handler;
}
void inject_event(std::unique_ptr<Event> event) {
  event_handlers[event->get_event_type()](event.get());
}
Matzi
  • 13,770
  • 4
  • 33
  • 50
  • Thanks, I though of that but I wanted to avoid the handler to have to do something extraneous. It's important for me to keep thing simple from the user of that event loop, even if it requires a little more work on the eventloop side. I though about the visitor but it seems that it would limit my ability to extend to user defined events. – Luke Skywalker Oct 05 '19 at 13:18