Система событий: наследование с приведением типов или объединениями - PullRequest
1 голос
/ 27 мая 2020

В настоящее время я работаю над системой событий для своего игрового движка. Я думал о двух способах реализации:

1. С наследованием:

class Event
{
    //...
    Type type; // Event type stored using an enum class
}

class KeyEvent : public Event
{
    int keyCode = 0;
    //...
}

2. С союзами:

class Event
{
    struct KeyEvent
    {
        int keyCode = 0;
        //...
    }

    Type type; // Event type stored using an enum class

    union {
        KeyEvent keyEvent;
        MouseButtonEvent mouseButtonEvent;
        //...
    }
}

В обоих случаях вы должны проверить перечисление Event::Type, чтобы узнать, какое событие произошло.

Когда вы используете наследование, вы должны преобразовать событие, чтобы получить его значения (например, преобразовать Event в KeyPressedEvent, чтобы получить код ключа). Поэтому вам нужно привести необработанные указатели к нужному типу, что часто очень подвержено ошибкам.

Когда вы используете объединение, вы можете просто получить событие, которое произошло. Но это также означает, что размер объединения в памяти всегда равен наибольшему возможному классу, содержащемуся в нем. Это означает, что много памяти может быть потрачено впустую.

Теперь к вопросу:

Есть ли какие-то аргументы, чтобы использовать один способ вместо другого? Стоит ли тратить память или рисковать неправильным кастингом?

Или есть лучшее решение, о котором я не думал?

В конце концов, я просто хочу передать, например, KeyEvent как Event. Затем я хочу проверить Event::Type с помощью класса enum. Если тип равен, скажем, Event::Type::Key, я хочу получить данные ключевого события.

Ответы [ 3 ]

3 голосов
/ 27 мая 2020

Event кажется более ориентированным на данные c, чем с поведением-центром c, поэтому я бы выбрал 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

, тогда использование может быть похоже на

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);
}
1 голос
/ 27 мая 2020

События разных типов обычно содержат очень разные данные и используются по-разному. Я бы сказал, что это плохо подходит для создания подтипов (наследования).

Давайте сначала проясним одну вещь: вам не нужно поле type в любом случае. В случае наследования вы можете определить подкласс вашего текущего объекта, применив его. При использовании с указателем dynamic_cast возвращает либо объект правильного подкласса, либо нулевой указатель. На cppreference есть хороший пример . В случае помеченного союза вы действительно должны использовать класс, который представляет помеченное объединение, например std::variant, вместо того, чтобы иметь type рядом с объединением в качестве членов вашего класса.

Если бы вы использовали наследование, типичный способ сделать это - поместить ваши события в массив (вектор) из Event указателей и выполнять действия с ними из интерфейса, предоставляемого классом Event. Однако, поскольку это события, вы, вероятно, не будете делать то же самое в зависимости от типа события, поэтому вы закончите преобразование текущего объекта Event в один из подклассов (KeyEvent) и только потом будете делать что-то с это из интерфейса, предоставляемого подклассом (используйте keyCode каким-то образом).

Если, однако, вы должны были использовать помеченное объединение или вариант, вы можете напрямую иметь свои Event объекты в массиве, и не придется иметь дело с указателями и понижением. Конечно, вы можете потратить впустую некоторое количество памяти, но насколько велики ваши классы событий? Кроме того, вы можете повысить производительность только потому, что объекты Event расположены близко друг к другу в памяти, в дополнение к тому, что вам не нужно разыменовать указатели.

Опять же, я бы сказал, что вариант лучше подходит для ситуации, чем подтипы, потому что события разных типов обычно используются по-разному. Например, KeyEvent имеет связанный ключ («код клавиши») среди сотен и не имеет позиции, тогда как MouseEvent имеет связанную кнопку из трех, может быть, пяти, но не намного больше, а также позицию. Я не могу придумать общего поведения между ними, кроме очень общих вещей, таких как временная метка или какое окно получает событие.

1 голос
/ 27 мая 2020
• 1000 , если все сделано правильно, кастинг вам не понадобится.

Что-то вроде:

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";
    }  
}

Результат:

I'm a KeyEvent
0 Key
I'm a MouseEvent
I have no keys

Это упрощенная версия, вам все равно придется иметь дело с конструкторами копирования, операторами присваивания и т. Д. .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...