更好的事件处理机制?

时间:2019-12-25 15:08:49

标签: c++ events event-handling

我正在为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;
    }
}

所以我的问题在于dynamic_cast函数中的Dispatch ing,也根据this的答案,如果我必须执行这种“解决方法”,那么有一个设计流在系统中,我应该重新考虑它而不是尝试对其进行修补!

有没有更好的方法来解决这个问题?

1 个答案:

答案 0 :(得分:0)

要对此进行一般性的构架,您需要有一堆类型擦除的对象(元素,事件),并且需要分派给处理程序以处理两种特定类型的对(例如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;
        /* ... */
}

选择

这两种方法都没有明显的整体优势,因此,我建议仅使用看起来更易于使用或维护的方法。

访客模式
  • 可访问的访客人数随着添加的类型而增加
  • 可以与C ++ 98一起使用
  • 异构容器必须存储动态分配的指向基本类型的指针
  • ...但是没有浪费的对象内存
std :: variant
  • 生成的调度代码的数量随类型的增加而增加
  • c ++ 11(通过带std的C ++ 17库)或更高版本
  • 可以将异构成员存储在连续内存中
  • ...但是如果对象大小不同,则会浪费内存。

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