Reputation: 1521
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.
Upvotes: 0
Views: 202
Reputation: 131515
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
:
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.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).Event
will not need to explicitly store or read its type - it'll always just know (unless you actively type-erase that is).Event*
's, you can just pass Event
s by value, or by move-references. So no more std::unique_ptr
s 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.
Upvotes: 2
Reputation: 7383
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.
Upvotes: 1
Reputation: 13925
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());
}
Upvotes: 0