1

I am writing an event system for a GUI, where i have an Event base class and a couple of derived classes (e.g. MouseDownEvent, MouseUpEvent, ...etc).

and each GUI element registers a callback for each type of event that it's supposed/wants to handle.

here is what a "typical" callback looks like:

bool OnMouseMove(const MouseMoveEvent& event);

and the event handling function looks something like this:

bool OnEvent(Event& event)
{
    EventsDispatcher dispatcher(event);
    dispatcher.Dispatch<MouseMoveEvent>(/* std::bind the callback */);
    /* ... */
}

and Dispatch looks something like this:

template<typename T, typename Callback>
bool Dispatch(Callback&& callback)
{
    try
    {
        return callback(dynamic_cast<T&>(m_Event));
    }
    catch (const std::bad_cast&)
    {
        return false;
    }
}

So my problem is with the dynamic_casting in the Dispatch function, also according to this answer, if i have to do this kind of "workaround" then there is a design flow in the system, and i should reconsider it instead of trying to patch it!

is there a better way to handle this?

  • Can you elaborate on that? –  Dec 25 '19 at 15:36
  • 1
    At the time when you emit the event you know the type of the even, so at the point of `bool OnEvent(Event& event)` you should know the type of the event. Registering the event listeners for a certain event, and finding the matching listeners for a certain event is indeed not an easy task, and results in such problems you have, but those should appear at a different point in the code. – t.niese Dec 25 '19 at 15:54
  • I saw an implementation for event dispatching that, iirc, worked with using only minimal "hacks", and was as close as you can get to good code. But I can't find it right now. I try to find it and to copy the relevant part as an answer. – t.niese Dec 25 '19 at 16:03

1 Answers1

0

To frame this generically, you have a bunch of type-erased objects (elements, events), and you need to dispatch to handlers for specific pairs of two types (e.g. ElementA and MouseUpEvent). This is a double dispatch problem, and there are two ways to deal with it in C++.

Double Dispatch Using The Visitor Pattern

This is the "classic" technique described in the 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. I can't make a better tutorial here, except to add that you can reduce the amount of boilerplate a lot by using variadic templates:

// 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)); }
};

After that, adding specifics for your setup can be done in just a few lines of code:

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; }
};

And to dispatch, just call the Accept method of an event:

void DispatchEvent(Event& event, Element& element) {
    event.Accept(element);
}

Double Dispatch Using Tagged Unions (std::variant)

Since C++17 (or C++11 with a 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 can be used to dispatch a callback based on the contained type of one or more variants. This happens to be really handy for double-dispatch.

First, use a variant to hold an event:

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

Then, a variant for each element, with methods to handle each event (use a base class to get rid of some boiler plate):

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>;

Then use std::visit with a generic lambda to dispatch:

void DispatchEvent(Event& event, Element& element) {
    std::visit([](auto&& el, auto&& ev) { return el.OnEvent(ev); }, element, event);
}

Ultimately, what this generates and optimizes to looks a bit like the event handler an old C GUI application:

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;
        /* ... */
}

Choosing

Neither method has a clear overall advantage performance-wise, so I recommend just using whichever one looks easier to use or maintain.

visitor pattern
  • vtable size of visitors increases as types added
  • can be used with C++98
  • heterogeneous container must store dynamically allocated pointers to base type
  • ... but no wasted object memory
std::variant
  • amount of generated dispatch code increases as types added
  • c++11 (via library, C++17 with std) or later
  • can store heterogeneous members in contiguous memory
  • ... but wasted memory if objects vary in size.

Demo of both: https://godbolt.org/z/MGp2No

parktomatomi
  • 3,851
  • 1
  • 14
  • 18