【问题标题】:Better event handling mechanism?更好的事件处理机制?
【发布时间】:2019-12-25 15:08:49
【问题描述】:

我正在为 GUI 编写一个事件系统,其中我有一个 Event 基类和几个派生类(例如 MouseDownEventMouseUpEvent、...等)。

并且每个 GUI 元素为它应该/想要处理的每种类型的事件注册一个回调。

这是“典型”回调的样子:

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_casting,同样根据this 的回答,如果我必须做这种“解决方法”,那么系统中有一个设计流程,并且我应该重新考虑它而不是尝试修补它!

有没有更好的方法来处理这个问题?

【问题讨论】:

  • 你能详细说明一下吗?
  • 在你发出事件的时候你知道偶数的类型,所以在bool OnEvent(Event&amp; event)点你应该知道事件的类型。为某个事件注册事件监听器,并为某个事件找到匹配的监听器确实不是一件容易的事,并且会导致您遇到这样的问题,但这些问题应该出现在代码中的不同位置。
  • 我看到了一个事件分派的实现,iirc 只使用了最少的“hacks”,并且尽可能接近好代码。但我现在找不到。我试图找到它并复制相关部分作为答案。

标签: c++ events event-handling


【解决方案1】:

一般来说,你有一堆类型擦除的对象(元素、事件),你需要为特定的两种类型对(例如ElementAMouseUpEvent)分派给处理程序。这是double dispatch的问题,在C++中有两种处理方式。

使用访问者模式的双重调度

这是Design Patterns 书中描述的“经典”技术。基本上,基本事件(节点)有一个虚拟方法,它分派到元素(访问者)中的向下转换的分派方法。访问者方法是分派到正确元素的虚拟方法。您可以在本书中、Wikipediabillion online tutorials 上阅读有关它的信息。我不能在这里做一个更好的教程,除了补充一点,你可以通过使用可变参数模板来减少样板的数量:

// 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 的 C++11)开始,您可以使用名为 std::variant 的类型安全联合,它可以在同一内存空间中保存一组固定的类型,并分派它们之间。一个名为std::visit 的全局方法可用于根据一个或多个变体的包含类型分派回调。这恰好对于双重调度非常方便。

首先,使用变体来举办活动:

struct MouseEvent { };
struct KeyEvent { };
using Event = std::variant<MouseEvent, KeyEvent>;

然后,每个元素都有一个变体,带有处理每个事件的方法(使用基类来摆脱一些样板):

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(通过库,带有 std 的 C++17)或更高版本
  • 可以将异构成员存储在连续内存中
  • ...但如果对象大小不同,则会浪费内存。

两者的演示:https://godbolt.org/z/MGp2No

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-03
    • 2019-03-25
    • 1970-01-01
    • 2010-12-05
    • 1970-01-01
    • 2012-05-13
    • 2016-01-16
    相关资源
    最近更新 更多