maniel34
maniel34

Reputation: 187

Event system: Inheritance with type-casting or unions

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

Answers (3)

Jarod42
Jarod42

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

anastaciu
anastaciu

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:

Live demo

#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

Nelfeal
Nelfeal

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

Related Questions