Доступ к типу времени выполнения полиморфного объекта с помощью switch и static_cast

Я хотел бы использовать шаблон асинхронного события, чтобы отделить мою программу. Я также хотел бы, чтобы базовый класс событий не обращал внимания на реализации, чтобы иметь самую широкую свободу передавать все, что мне нравится, по событию. Поэтому использование static_cast внутри переключателя кажется мне простым и, возможно, безопасным решением:

enum class EventType
{
    None,
    EventA,
    EventB
};

class BaseEvent
{
    public:
        BaseEvent(EventType t = EventType::None) : type(t) { }
        virtual ~BaseEvent() {}
        auto get_type() { return type; }
    private:
        EventType type;

    // Oblivious and clean interface
};

class EventA : public BaseEvent
{
    public:
        EventA() : BaseEvent(EventType::EventA) { }

    // ... whatever I like
};
class EventB : public BaseEvent
{
    public:
        EventB() : BaseEvent(EventType::EventB) { }

    // ... whatever I like
};

void handle_event(BaseEvent* pe)
{
    switch (pe->get_type())
    {
        case EventType::EventA:
        {
            EventA* original_a = static_cast<EventA*>(pe);

            // In this case I know what is "pe" and what 
            // operations and data I can access and use.

            break;
        }
        case EventType::EventB:
        {
            EventB* original_b = static_cast<EventB*>(pe);

            //...

            break;
        }
    }
}

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

case EventType::EventA:

согласуется со следующей строкой

EventA* original_a = static_cast<EventA*>(pe);

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

Я знаю, что мог бы использовать массив или вектор std :: variant в базовом классе, но это кажется довольно ограничивающим в отношении возможных реализаций производных событий. Я также мог бы использовать карту для хранения имен параметров и их значений, но это кажется довольно медленным и недружелюбным к памяти, а также ограничивает возможные типы параметров.

В качестве альтернативы я мог бы также использовать dynamic_cast, хотя у него есть накладные расходы, но, возможно, он окупает затраты, увеличивая ремонтопригодность.

РЕДАКТИРОВАТЬ

Чтобы быть кратким, я забыл упомянуть некоторые важные детали вопроса:

  • События должны быть полиморфно поставлены в очередь в контейнере, поэтому я думаю, что CRTP это невозможно.
  • Контекст - это имитация на основе агентов в реальном времени (видеоигра), где у меня есть основной цикл, повторяющий события. События могут запускаться в любом месте на каждой итерации. Они будут обрабатываться определенными обработчиками в следующей итерации (ах).

    std::queue<BaseEvent*> past_events;
    
    int main()
    {
        while (true)
        {
            while (!past_events.empty())
            {
                handle_event(past_events.front());
    
                //handle_event2(...)
                //handle_event3(...)
                //...
    
                past_events.pop();
            }
    
            // New events are fired...
        }
    }
    

Взгляните на CRTP, если вы хотите иметь статический полиморфизм.

πάντα ῥεῖ 27.10.2018 14:35

Также обратите внимание, что такие конструкции обычно являются явным признаком несовершенного дизайна.

πάντα ῥεῖ 27.10.2018 14:39

Это широко распространенная практика (даже в коде C). В нем даже есть несколько библиотечных подпрограмм. См. RTTI в стиле LLVM.

user7860670 27.10.2018 14:45

@ πάνταῥεῖ Спасибо за предложение. Я добавил больше деталей о контексте. Я думаю, что при использовании CRTP производное от BaseEvent нельзя хранить в контейнере. Это правильно?

Tarquiscani 27.10.2018 17:22

@VTT Спасибо, статью прочитал только сейчас. Это заставляет меня более уверенно относиться к шаблону.

Tarquiscani 27.10.2018 23:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
5
52
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Анализ вашего дизайна

На первый взгляд, можно подумать, что это неоптимальный дизайн, потому что вы не используете полиморфизм, чтобы позволить событию выполнять правильное действие, вместо того, чтобы позволить обработчику события переключаться и приводить его. Но при чтении ваших аргументов вырисовывается другая картина:

Вы решили специально поместить логику обработки событий в обработчик событий. Это позволяет отделить обработку события от самого события. Другими словами, разные обработчики событий могут иметь совершенно разное поведение для одного и того же события (в зависимости от контекста, приемника событий, приложения и т. д.), Точно так же, как каждое приложение Windows имеет некоторый цикл событий и реагирует на одни и те же события в Совершенно иначе.

Таким образом, вы сознательно решили не помещать поведение в событие, и поэтому вы не можете использовать полиморфизм в событии. Не зная контекста, сложно посоветовать другой подход.

Последствия

Поскольку вы определили логику получения типа события в базовом классе, вы можете предположить, что вы достаточно хорошо знаете его тип, и выбрали static_cast. Но...

Риск номер 1

Однако существует серьезный риск того, что однажды будет создан новый тип события с неправильным типом события (копирование и вставка, опечатка и т. д.). Это может привести к UB.

Снижение риска:

  • используйте dynamic_cast для перехвата таких несоответствий в каждом обработчике событий во время выполнения. Обратите внимание, что специалисты по обслуживанию могут забыть об этом, так что это снижение риска, а не предотвращение риска.
  • или предвидеть набор тестов, который создает все типы событий и выполняет проверку согласованности dynamic_cast во время сборки.

Риск номер 2

У вас может быть некоторая непреднамеренная копия события (конструктор копии или назначение) с или без нарезки, которая случайно перезаписывает реальный тип события (например, if (*eventA=*eventB) /* ouch!! == */).

Снижение риска: удалите конструктор копирования и назначение из базового класса событий, чтобы предотвратить такие аварии.

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

Tarquiscani 27.10.2018 17:17

@Tarquiscani Я вижу, что в вашем основном цикле у вас есть несколько обработчиков событий, которые могут оправдать ваш дизайн. Множественная отправка (т.е. полиморфизм зависит от обработчика событий и события) потребует связи между обработчиками и событием и будет иметь аналогичные недостатки при добавлении новых типов событий. Другие альтернативы (например, диспетчеризация, управляемая таблицей) будут не менее подвержены ошибкам в обслуживании. Так что да, я думаю, что мой анализ все еще в силе. И да, я настоятельно рекомендую предложенные меры по смягчению последствий.

Christophe 27.10.2018 17:28

Другие вопросы по теме