Я хотел бы использовать шаблон асинхронного события, чтобы отделить мою программу. Я также хотел бы, чтобы базовый класс событий не обращал внимания на реализации, чтобы иметь самую широкую свободу передавать все, что мне нравится, по событию. Поэтому использование 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
, хотя у него есть накладные расходы, но, возможно, он окупает затраты, увеличивая ремонтопригодность.
Чтобы быть кратким, я забыл упомянуть некоторые важные детали вопроса:
Контекст - это имитация на основе агентов в реальном времени (видеоигра), где у меня есть основной цикл, повторяющий события. События могут запускаться в любом месте на каждой итерации. Они будут обрабатываться определенными обработчиками в следующей итерации (ах).
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...
}
}
Также обратите внимание, что такие конструкции обычно являются явным признаком несовершенного дизайна.
Это широко распространенная практика (даже в коде C). В нем даже есть несколько библиотечных подпрограмм. См. RTTI в стиле LLVM.
@ πάνταῥεῖ Спасибо за предложение. Я добавил больше деталей о контексте. Я думаю, что при использовании CRTP производное от BaseEvent
нельзя хранить в контейнере. Это правильно?
@VTT Спасибо, статью прочитал только сейчас. Это заставляет меня более уверенно относиться к шаблону.
На первый взгляд, можно подумать, что это неоптимальный дизайн, потому что вы не используете полиморфизм, чтобы позволить событию выполнять правильное действие, вместо того, чтобы позволить обработчику события переключаться и приводить его. Но при чтении ваших аргументов вырисовывается другая картина:
Вы решили специально поместить логику обработки событий в обработчик событий. Это позволяет отделить обработку события от самого события. Другими словами, разные обработчики событий могут иметь совершенно разное поведение для одного и того же события (в зависимости от контекста, приемника событий, приложения и т. д.), Точно так же, как каждое приложение Windows имеет некоторый цикл событий и реагирует на одни и те же события в Совершенно иначе.
Таким образом, вы сознательно решили не помещать поведение в событие, и поэтому вы не можете использовать полиморфизм в событии. Не зная контекста, сложно посоветовать другой подход.
Поскольку вы определили логику получения типа события в базовом классе, вы можете предположить, что вы достаточно хорошо знаете его тип, и выбрали static_cast
. Но...
Однако существует серьезный риск того, что однажды будет создан новый тип события с неправильным типом события (копирование и вставка, опечатка и т. д.). Это может привести к UB.
Снижение риска:
dynamic_cast
для перехвата таких несоответствий в каждом обработчике событий во время выполнения. Обратите внимание, что специалисты по обслуживанию могут забыть об этом, так что это снижение риска, а не предотвращение риска.dynamic_cast
во время сборки.У вас может быть некоторая непреднамеренная копия события (конструктор копии или назначение) с или без нарезки, которая случайно перезаписывает реальный тип события (например, if (*eventA=*eventB) /* ouch!! == */
).
Снижение риска: удалите конструктор копирования и назначение из базового класса событий, чтобы предотвратить такие аварии.
Спасибо за анализ. Я отредактировал вопрос, добавив более подробную информацию о контексте. Считаете ли вы, что это все еще осуществимая модель (часть рисков, которые вы подчеркнули)?
@Tarquiscani Я вижу, что в вашем основном цикле у вас есть несколько обработчиков событий, которые могут оправдать ваш дизайн. Множественная отправка (т.е. полиморфизм зависит от обработчика событий и события) потребует связи между обработчиками и событием и будет иметь аналогичные недостатки при добавлении новых типов событий. Другие альтернативы (например, диспетчеризация, управляемая таблицей) будут не менее подвержены ошибкам в обслуживании. Так что да, я думаю, что мой анализ все еще в силе. И да, я настоятельно рекомендую предложенные меры по смягчению последствий.
Взгляните на CRTP, если вы хотите иметь статический полиморфизм.