更好的事件处理机制?
Better event handling mechanism?
我正在为 GUI 编写一个事件系统,其中我有一个 Event
基础 class 和几个派生的 classes(例如 MouseDownEvent
,MouseUpEvent
, ...等等).
并且每个 GUI 元素都为它 supposed/wants 处理的每种类型的事件注册一个回调。
这里是 "typical" 回调的样子:
bool OnMouseMove(const MouseMoveEvent& event);
事件处理函数看起来像这样:
bool OnEvent(Event& event)
{
EventsDispatcher dispatcher(event);
dispatcher.Dispatch<MouseMoveEvent>(/* std::bind the callback */);
/* ... */
}
和 Dispatch
看起来像这样:
template<typename T, typename Callback>
bool Dispatch(Callback&& callback)
{
try
{
return callback(dynamic_cast<T&>(m_Event));
}
catch (const std::bad_cast&)
{
return false;
}
}
所以我的问题是 Dispatch
函数中的 dynamic_cast
ing,也是根据 this 的回答,如果我必须做这种 "workaround" 那么系统中有一个设计流程,我应该重新考虑它而不是试图修补它!
有没有更好的方法来处理这个问题?
为了通用地构建这个框架,您有一堆类型擦除的对象(元素、事件),并且您需要为特定的两种类型对(例如 ElementA
和 MouseUpEvent
).这是一个double dispatch问题,在C++中有两种处理方法。
使用访问者模式的双重调度
这是Design Patterns book. Basically, the base event (node) has a virtual method that dispatches to a downcasted dispatch method in a element (visitor). The visitor methods are virtual methods which dispatch to the right element. You can read about it in the book, on Wikipedia, or a billion online tutorials中描述的"classic"技术。我不能在这里做一个更好的教程,除了补充一点,你可以通过使用可变模板来减少样板文件的数量:
// Visitor
template <typename... TNodes>
struct GenericVisitor;
template <typename TNode, typename... TNodes>
struct GenericVisitor<TNode, TNodes...> : public GenericVisitor<TNodes...> {
virtual bool Visit(const TNode&) { return false; }
};
template <>
struct GenericVisitor<> {
virtual ~GenericVisitor() = default;
};
// Node
template <typename TVisitor>
struct GenericBaseNode {
virtual bool Accept(TVisitor& visitor) const = 0;
};
template <typename TNode, typename TVisitor>
struct GenericNode : public GenericBaseNode<TVisitor> {
virtual bool Accept(TVisitor& visitor) const { return visitor.Visit(*static_cast<TNode*>(this)); }
};
之后,只需几行代码即可为您的设置添加细节:
struct MouseEvent;
struct KeyEvent;
using Element = GenericVisitor<MouseEvent, KeyEvent>;
using Event = GenericBaseNode<Element>;
struct MouseEvent : public GenericNode<MouseEvent, Element> { };
struct KeyEvent : public GenericNode<KeyEvent, Element> { };
struct ElementA : public Element {
virtual bool Visit(const MouseEvent&) { return true; }
};
要派发,只需调用事件的 Accept
方法:
void DispatchEvent(Event& event, Element& element) {
event.Accept(element);
}
使用标记联合的双重分派 (std::variant)
自从 C++17(或带有 single-header library), you can use a type-safe union called std::variant
, which can hold a fixed set of types in the same memory space, and dispatch between them. A global method called std::visit
的 C++11 可用于根据包含的一个或多个变体的类型分派回调。这对于双派遣。
首先,使用变体举办活动:
struct MouseEvent { };
struct KeyEvent { };
using Event = std::variant<MouseEvent, KeyEvent>;
然后,每个元素的变体,以及处理每个事件的方法(使用基 class 来摆脱一些样板):
struct BaseElement {
template <typename TEvent>
bool OnEvent(const TEvent&) { return false; }
};
struct ElementA : public BaseElement {
using BaseElement::OnEvent;
bool OnEvent(const MouseEvent&) { return true; }
};
using Element = std::variant<ElementA>;
然后使用 std::visit
和通用 lambda 来调度:
void DispatchEvent(Event& event, Element& element) {
std::visit([](auto&& el, auto&& ev) { return el.OnEvent(ev); }, element, event);
}
最终,生成和优化的内容看起来有点像旧 C GUI 应用程序的事件处理程序:
struct Event { int id; union { MouseEvent AsMouseEvent; KeyEvent AsKeyEvent; }; };
struct Element { int id; union { ElementA AsElementA; }; };
switch (event.id) {
case MouseEvent:
switch(element.id) {
case ElementA:
return element.AsElementA.OnEvent(event.AsMouseEvent);
/*...*/
}
break;
/* ... */
}
正在选择
这两种方法在性能方面都没有明显的整体优势,因此我建议只使用看起来更易于使用或维护的一种。
访客模式
- 访问者的 vtable 大小随着类型的增加而增加
- 可以与 C++98 一起使用
- 异构容器必须存储指向基类型的动态分配指针
- ...但没有浪费对象内存
std::variant
- 生成的调度代码数量随着类型的增加而增加
- c++11(通过库,C++17 with std)或更高版本
- 可以在连续内存中存储异构成员
- ...但如果对象大小不同,则会浪费内存。
我正在为 GUI 编写一个事件系统,其中我有一个 Event
基础 class 和几个派生的 classes(例如 MouseDownEvent
,MouseUpEvent
, ...等等).
并且每个 GUI 元素都为它 supposed/wants 处理的每种类型的事件注册一个回调。
这里是 "typical" 回调的样子:
bool OnMouseMove(const MouseMoveEvent& event);
事件处理函数看起来像这样:
bool OnEvent(Event& event)
{
EventsDispatcher dispatcher(event);
dispatcher.Dispatch<MouseMoveEvent>(/* std::bind the callback */);
/* ... */
}
和 Dispatch
看起来像这样:
template<typename T, typename Callback>
bool Dispatch(Callback&& callback)
{
try
{
return callback(dynamic_cast<T&>(m_Event));
}
catch (const std::bad_cast&)
{
return false;
}
}
所以我的问题是 Dispatch
函数中的 dynamic_cast
ing,也是根据 this 的回答,如果我必须做这种 "workaround" 那么系统中有一个设计流程,我应该重新考虑它而不是试图修补它!
有没有更好的方法来处理这个问题?
为了通用地构建这个框架,您有一堆类型擦除的对象(元素、事件),并且您需要为特定的两种类型对(例如 ElementA
和 MouseUpEvent
).这是一个double dispatch问题,在C++中有两种处理方法。
使用访问者模式的双重调度
这是Design Patterns book. Basically, the base event (node) has a virtual method that dispatches to a downcasted dispatch method in a element (visitor). The visitor methods are virtual methods which dispatch to the right element. You can read about it in the book, on Wikipedia, or a billion online tutorials中描述的"classic"技术。我不能在这里做一个更好的教程,除了补充一点,你可以通过使用可变模板来减少样板文件的数量:
// Visitor
template <typename... TNodes>
struct GenericVisitor;
template <typename TNode, typename... TNodes>
struct GenericVisitor<TNode, TNodes...> : public GenericVisitor<TNodes...> {
virtual bool Visit(const TNode&) { return false; }
};
template <>
struct GenericVisitor<> {
virtual ~GenericVisitor() = default;
};
// Node
template <typename TVisitor>
struct GenericBaseNode {
virtual bool Accept(TVisitor& visitor) const = 0;
};
template <typename TNode, typename TVisitor>
struct GenericNode : public GenericBaseNode<TVisitor> {
virtual bool Accept(TVisitor& visitor) const { return visitor.Visit(*static_cast<TNode*>(this)); }
};
之后,只需几行代码即可为您的设置添加细节:
struct MouseEvent;
struct KeyEvent;
using Element = GenericVisitor<MouseEvent, KeyEvent>;
using Event = GenericBaseNode<Element>;
struct MouseEvent : public GenericNode<MouseEvent, Element> { };
struct KeyEvent : public GenericNode<KeyEvent, Element> { };
struct ElementA : public Element {
virtual bool Visit(const MouseEvent&) { return true; }
};
要派发,只需调用事件的 Accept
方法:
void DispatchEvent(Event& event, Element& element) {
event.Accept(element);
}
使用标记联合的双重分派 (std::variant)
自从 C++17(或带有 single-header library), you can use a type-safe union called std::variant
, which can hold a fixed set of types in the same memory space, and dispatch between them. A global method called std::visit
的 C++11 可用于根据包含的一个或多个变体的类型分派回调。这对于双派遣。
首先,使用变体举办活动:
struct MouseEvent { };
struct KeyEvent { };
using Event = std::variant<MouseEvent, KeyEvent>;
然后,每个元素的变体,以及处理每个事件的方法(使用基 class 来摆脱一些样板):
struct BaseElement {
template <typename TEvent>
bool OnEvent(const TEvent&) { return false; }
};
struct ElementA : public BaseElement {
using BaseElement::OnEvent;
bool OnEvent(const MouseEvent&) { return true; }
};
using Element = std::variant<ElementA>;
然后使用 std::visit
和通用 lambda 来调度:
void DispatchEvent(Event& event, Element& element) {
std::visit([](auto&& el, auto&& ev) { return el.OnEvent(ev); }, element, event);
}
最终,生成和优化的内容看起来有点像旧 C GUI 应用程序的事件处理程序:
struct Event { int id; union { MouseEvent AsMouseEvent; KeyEvent AsKeyEvent; }; };
struct Element { int id; union { ElementA AsElementA; }; };
switch (event.id) {
case MouseEvent:
switch(element.id) {
case ElementA:
return element.AsElementA.OnEvent(event.AsMouseEvent);
/*...*/
}
break;
/* ... */
}
正在选择
这两种方法在性能方面都没有明显的整体优势,因此我建议只使用看起来更易于使用或维护的一种。
访客模式- 访问者的 vtable 大小随着类型的增加而增加
- 可以与 C++98 一起使用
- 异构容器必须存储指向基类型的动态分配指针
- ...但没有浪费对象内存
- 生成的调度代码数量随着类型的增加而增加
- c++11(通过库,C++17 with std)或更高版本
- 可以在连续内存中存储异构成员
- ...但如果对象大小不同,则会浪费内存。