Reputation: 187
I am currently working on an event system for my game engine. I've thought about two ways I could implement it:
1. With inheritance:
class Event
{
//...
Type type; // Event type stored using an enum class
}
class KeyEvent : public Event
{
int keyCode = 0;
//...
}
2. With unions:
class Event
{
struct KeyEvent
{
int keyCode = 0;
//...
}
Type type; // Event type stored using an enum class
union {
KeyEvent keyEvent;
MouseButtonEvent mouseButtonEvent;
//...
}
}
In both cases you have to check the Event::Type
enum, to know which kind of event has occured.
When you use inheritance, you have to cast the event to get it's values (e.g. cast Event
to KeyPressedEvent
to get key code). So you have to cast raw pointers to the neeeded type, which is often very error prone.
When you use the union you can simply retrieve the event that has occured. But this does also mean that the size of the union in memory is always as big as it's largest possible class contained in it. This means that a lot of memory may be wasted.
Now to the question:
Are there any arguments to use one way over the other? Should I waste memory or take the risk of wrong casting?
Or is there a better solution to this which I haven't thought of?
In the end I just want to pass e.g. a KeyEvent
as an Event
. Then I want to check the Event::Type
using the enum class. If the type is equal to say Event::Type::Key
I want to get the data of the key event.
Upvotes: 1
Views: 821
Reputation: 217478
Event
seems more data-centric than behavior-centric, so I would choose std::variant
:
using Event = std::variant<KeyEvent, MouseButtonEvent/*, ...*/>;
// or using Event = std::variant<std::unique_ptr<KeyEvent>, std::unique_ptr<MouseButtonEvent>/*, ...*/>;
// In you are concerned to the size... at the price of extra indirection+allocation
then usage might be similar to
void Print(const KeyEvent& ev)
{
std::cout << "KeyEvent: " << ev.key << std::endl;
}
void Print(const MouseButtonEvent& ev)
{
std::cout << "MouseButtonEvent: " << ev.button << std::endl;
}
// ...
void Print_Dispatch(const Event& ev)
{
std::visit([](const auto& ev) { return Print(ev); }, ev);
}
Upvotes: 3
Reputation: 23802
You can use polymorphism, I believe it's a solution if your derived classes share a good amount of methods, you can have an abstract class of events, derive from it and implement these methods, you can then have them in the same container, if done correctly you won't need casting.
Something along the lines of:
#include <vector>
#include <iostream>
#include <memory>
class Event {//abstract class
public:
virtual void whatAmI() = 0; //pure virtual defined in derived classes
virtual int getKey() = 0;
virtual ~Event(){}
};
class KeyEvent : public Event {
int keyCode = 0;
public:
void whatAmI() override { std::cout << "I'm a KeyEvent" << std::endl; }
int getKey() override { return keyCode; }
};
class MouseEvent : public Event {
int cursorX = 0, cursorY = 0;
public:
void whatAmI() override { std::cout << "I'm a MouseEvent" << std::endl; }
int getKey() override { throw -1; }
};
int main() {
std::vector<std::unique_ptr<Event>> events;
events.push_back(std::make_unique<KeyEvent>()); //smart pointers
events.push_back(std::make_unique<MouseEvent>());
try{
for(auto& i : events) {
i->whatAmI();
std::cout << i->getKey() << " Key" << std::endl;
}
} catch(int i){
std::cout << "I have no keys";
}
}
Output:
I'm a KeyEvent
0 Key
I'm a MouseEvent
I have no keys
This is a simplified version, you would still have to deal with copy constructors, assignment operators and such.
Upvotes: 1
Reputation: 13269
Events of different types typically contain very different data and have very different usage. I would say this is a bad fit for subtyping (inheritance).
Let's clear up one thing first: you don't need the type
field in either case. In the case of inheritance, you can determine the subclass of you current object by casting it. When used on a pointer, dynamic_cast
returns either an object of the correct subclass, or a null pointer. There is a good example on cppreference. In the case of a tagged union, you should really use a class that represents a tagged union, like std::variant
, instead of having a type
next to a union as members of you class.
If you were to use inheritance, the typical way to do it is to have your events in an array (vector) of Event
pointers, and do things with them from the interface provided by the Event
class. However, since these are events, you probably won't do the same things depending on the event type, so you will end up downcasting the current Event
object into one of the subclasses (KeyEvent
) and only then do stuff with it from the interface provided by the subclass (use keyCode
in some way).
If however you were to use a tagged union, or variant, you can directly have your Event
objects in an array, and not have to deal with pointers and downcasting. Sure, you might waste some amount of memory, but how big are your event classes anyway? Plus, you might gain performance just because the Event
objects are close together in memory, in addition to not having to dereference pointers.
Again, I would say a variant is better suited to the situation than subtyping because events of different types are usually used in very different ways. For example, a KeyEvent
has an associated key ("key code") among hundreds, and no position, whereas a MouseEvent
has an associated button among three, maybe five but not many more, as well as a position. I can't think of a common behavior between the two, apart from very general things like a timestamp or which window receives the event.
Upvotes: 1