I'm attempting to mimic JavaScript style Event/EventTarget
operations in C++ (C++17).
I have an Event
base class that works as needed, and a KeyboardEvent
class that inherits from Event
.
My EventTarget.addEventListener
function stores EventObjects
in a vector that can be called with dispatchEvent
.
I am able to use addEventListener
to store callback functions using a Event
parameter, e.g. onGenericEvent(Event event)
.
However, I am unable to use addEventListener
to add callback functions that take a KeyboardEvent
parameter, e.g. onKeyDown(KeyboardEvent event)
.
I was expecting std::function<void(Event)>
to accept KeyboardEvent
since it inherits from Event
but I receive a compilation error:
ERROR: cannot convert 'void (*)(KeyboardEvent)' to 'std::function<void(Event)>'
How should I proceed?
#include <iostream>
#include <functional>
#include <string>
#include <vector>
class Event {
public:
Event(std::string _type, void* _pointer) {
type = _type;
pointer = _pointer;
}
std::string type;
void* pointer;
};
struct EventObject {
std::string type;
std::function<void(Event)> listener;
void* pointer;
};
class EventTarget {
public:
static void addEventListener(std::string type, std::function<void(Event)> listener, void* pointer){
EventObject eventObj;
eventObj.type = type;
eventObj.listener = listener;
eventObj.pointer = pointer;
events.push_back(eventObj);
}
static void dispatchEvent(Event event){
for (unsigned int n = 0; n < events.size(); ++n) {
if (event.type == events[n].type && event.pointer == events[n].pointer) {
events[n].listener(event);
}
}
}
static std::vector<EventObject> events;
};
std::vector<EventObject> EventTarget::events;
class KeyboardEvent : public Event {
public:
KeyboardEvent(std::string _type, void* _pointer) : Event(_type, _pointer) {
key = "random key";
}
std::string key;
};
class App
{
public:
static void onGenericEvent(Event event) {
printf("event.type = %s \n", event.type.c_str());
}
static void onKeyDown(KeyboardEvent event) {
printf("event.type = %s \n", event.type.c_str()); // print Event class property
printf("event.key = %s \n", event.key.c_str()); // print KeyboardEvent class property
}
};
int main() {
App* app = new App();
EventTarget::addEventListener("generic", &App::onGenericEvent, app);
Event event("generic", app);
EventTarget::dispatchEvent(event); // simulate generic event
EventTarget::addEventListener("keydown", &App::onKeyDown, app); //ERROR: cannot convert 'void (*)(KeyboardEvent)' to std::function<void(Event)>'
KeyboardEvent keyEvent("keydown", app);
EventTarget::dispatchEvent(keyEvent); // simulate keydown event
}
Online test code: https://onlinegdb.com/uJBz6g4lr
Note: First and foremost you are going to slice your Event
objects if you pass them around by value like that. You need to pass references or pointers for polymorphism to work!
void(KeyboardEvent&)
is more specific than void(Event&)
, so std::function<void(Event&)>
cannot wrap a void(KeyboardEvent&)
.
Consider if it could, then this would be possible:
void handler(KeybaordEvent& evt)
{
std::cout << evt.key;
}
int main()
{
std::function<void(Event&)> callback(&handler);
MouseEvent mouseEvent; // Imagine MouseEvent also derives from Event
callback(mouseEvent);
}
Since a MouseEvent
is an Event
a std::function<void(Event&)>
can accept a MouseEvent
parameter. If it was allowed to wrap a void(KeyboardEvent&)
that would result in the handler being passed the wrong type. For that reason, what you want is not allowed.
Remember, type checking is done at compile time. Just because you (try to) never pass the wrong type by checking the EventObject
's type
member at runtime doesn't mean you can have mismatched types at compile time.
There are two ways to deal with this:
Event
and do a runtime check that they got the type of event that they were expecting (i.e. using dynamic_cast
).As an example of the second option, you could so something like this:
template <typename EventType>
struct EventObject {
std::string type;
std::function<void(EventType)> listener;
void* pointer;
};
class EventTarget {
public:
template <typename EventType>
static void addEventListener(std::string type, std::function<void(EventType)> listener, void* pointer){
EventObject<EventType> eventObj;
eventObj.type = type;
eventObj.listener = listener;
eventObj.pointer = pointer;
events<EventType>.push_back(eventObj);
}
template <typename EventType>
static void dispatchEvent(EventType event){
for (auto& eventObj : events<EventType>) {
eventObj.listener(event);
}
}
template <typename EventType>
static std::vector<EventObject<EventType>> events;
};
template <typename EventType>
std::vector<EventObject<EventType>> EventTarget::events;
class KeyboardEvent : public Event {
public:
KeyboardEvent(std::string _type, void* _pointer) : Event(_type, _pointer) {
key = "random key";
}
std::string key;
};
class App
{
public:
static void onGenericEvent(Event event) {
printf("event.type = %s \n", event.type.c_str());
}
static void onKeyDown(KeyboardEvent event) {
printf("event.type = %s \n", event.type.c_str()); // print Event class property
printf("event.key = %s \n", event.key.c_str()); // print KeyboardEvent class property
}
};
int main() {
App* app = new App();
EventTarget::addEventListener<Event>("generic", &App::onGenericEvent, app);
Event event("generic", app);
EventTarget::dispatchEvent(event); // simulate generic event
EventTarget::addEventListener<KeyboardEvent>("keydown", &App::onKeyDown, app); //ERROR: cannot convert 'void (*)(KeyboardEvent)' to std::function<void(Event)>'
KeyboardEvent keyEvent("keydown", app);
EventTarget::dispatchEvent(keyEvent); // simulate keydown event
}
This creates separate events
vectors for each type of event. Now the compiler can check at compile time that the right type of event will always be passed to each handler.