1

I have been playing around with the new C++11 standard lately and decided to create a basic event-handling system. The code below provides a small example of my current implementation.

#include <functional>
#include <vector>
#include <iostream>

template <typename Event>
class EventBroadcaster
{
public:
    typedef std::function<void(const Event&)> Connection;

    void connect(Connection&& connection)
    {
        connections.push_back(std::move(connection));
    }

    void signal(const Event& event)
    {
        for (const auto& connection : connections)
        {
            connection(event);
        }
    }
private:
    std::vector<Connection> connections;
};

struct MouseMotion
{
    int x = 0;
    int y = 0;
};

class Input : public EventBroadcaster<MouseMotion>
{
public:
    void process()
    {
        MouseMotion mouseMotion;
        mouseMotion.x = 10;
        mouseMotion.y = 20;
        signal(mouseMotion);
    }
};

int main()
{
    int x = 0;
    int y = 0;

    Input input;

    input.connect([&](const MouseMotion& e){
        x += e.x;
        y += e.y;
    });

    input.process();

    std::cout << x << "," << y << std::endl; // Output: 10,20

    return 0;
}

The above solution does work quite nicely if the Input class would only broadcast a single event. There might however be the case that the Input class would want to be able to send KeyPress events besides just MouseMotion events.

I thought about using multiple inheritance. Making Input inherit both EventBroadcaster<MouseMotion> and EventBroadcaster<KeyPress>. This results in compiler errors warning of ambiguous functions. The solution provided in the following answer Multiple Inheritance Template Class does work for the protected signal function, but not for the public connect function called outside the Input class.

Besides multiple inheritance I wondered if variadic-templates could help my with my problem. I have looked at (partial) template specialization and unpacking variadic-templates. But have been unable to come with an (elegant) solution.

What would be the best way to support multiple event types?

Community
  • 1
  • 1
daniel
  • 25
  • 1
  • 5
  • How come the solution you linked does not work for `connect()`? – Andy Prowl Feb 22 '13 at 16:23
  • Adding the following to the Input class doesn't work:`using EventBroadcaster::signal; using EventBroadcaster::signal; using EventBroadcaster::connect; using EventBroadcaster::connect;` It does however work for MouseMotion events after I just remove the `using EventBroadcaster::connect;` line. But than remains the problem of not being able to connect KeyPress events. – daniel Feb 22 '13 at 16:26
  • Not an answer to your question, but `connections.push_back(std::move(connection));` would be more efficient, and calling the function can be just `connection(event)` without the extra parentheses – Jonathan Wakely Feb 25 '13 at 13:57
  • @JonathanWakely Why not `emplace_back()`? – user1095108 Feb 26 '13 at 13:29
  • @user1095108, `push_back` is fewer characters to type and there's no advantage to `emplace_back` in this case so why use it? – Jonathan Wakely Feb 26 '13 at 13:32

2 Answers2

4

I'd make EventBroadcaster<T> an implementation detail, and instead expose EventBroadcasters<Ts...>.

EventBroadcasters<Ts...> owns EventBroadcaster<Ts>... has template methods connect<U> and signal<U>, and forwards these to the EventBroadcaster<U>. This should fail to compile if U is not in Ts....

So now you signal(mouseMotion), which resolves to signal<MouseMotion>(mouseMotion), which connects through to the correct broadcaster.

When you connect to listen, similarly so long as you are using the right kind of std::function things work. And if not, you can just pass in the type of the Event you are listening for.

There are ways to make it even more magical, and enable full-on proper overload resolution that you'd want, but that gets really tricky. Ie, you instrument up connect to do SFINAE and manually dispatch the passed in lambda (by examining its operator()) to the correct std::function override. However, simply being able to say connect<MouseMotion>([&](MouseMotion m){...}) should be an improvement.

(SFINAE technique would consist of checking which std::function type out of a list can be constructed from your passed in U, and if there is one unique such, using that one. Also, check if U is such a std::function (or a cv variant of such). It isn't ridiculously hard, but it is very messy looking, and in my experience most people don't like the type-spew and prefer to just specify <MouseMotion>.)

UPDATE:

The following code block provides an implementation of this answer.

#include <functional>
#include <vector>
#include <iostream>

namespace detail
{
    template <typename Event>
    class EventBroadcaster
    {
    public:
        typedef std::function<void(const Event&)> Connection;

        void connect(Connection&& connection)
        {
            connections.push_back(std::move(connection));
        }

        void signal(const Event& event)
        {
            for (const auto& connection : connections)
            {
                connection(event);
            }
        }
    private:
        std::vector<Connection> connections;
    };

   template <typename T> struct traits
       : public traits<decltype(&T::operator())> {};

   template <typename C, typename R, typename A>
   struct traits<R(C::*)(const A&) const>
   {
       typedef A type;
   };
}

template <typename... Events>
class EventBroadcaster
   : detail::EventBroadcaster<Events>...
{
public:
    template <typename Connection>
    void connect(Connection&& connection)
    {
        typedef typename detail::traits<Connection>::type Event;
        detail::EventBroadcaster<Event>& impl = *this;
        impl.connect(std::move(connection));
    }

    template <typename Event>
    void signal(const Event& event)
    {
        detail::EventBroadcaster<Event>& impl = *this;
        impl.signal(event);
    }

    virtual void processEvents() = 0;
};

struct MouseMotion
{
    int x = 0;
    int y = 0;
};

struct KeyPress
{
    char c;
};

class Input
    : public EventBroadcaster<MouseMotion, KeyPress>
{
public:
    void processEvents()
    {
        MouseMotion mouseMotion;
        mouseMotion.x = 10;
        mouseMotion.y = 20;
        signal(mouseMotion);

        KeyPress keyPress;
        keyPress.c = 'a';
        signal(keyPress);
    }
};

int main()
{
    int x = 0;
    int y = 0;
    char c = '~';

    Input input;

    input.connect([&](const MouseMotion& e){
        x += e.x;
        y += e.y;
    });

    input.connect([&](const KeyPress& e){
        c = e.c;
    });

    input.processEvents();

    std::cout << c << " " << x << "," << y << std::endl; // Output: a 10,20

    return 0;
}
daniel
  • 25
  • 1
  • 5
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

If that is OK to you, you can solve this problem by adding the using directives for KeyPress in your Input class and store your lambda in a std::function<> object rather than using auto:

class Input :
    public EventBroadcaster<MouseMotion>,
    public EventBroadcaster<KeyPress>
{
public:
    using EventBroadcaster<MouseMotion>::signal;
    using EventBroadcaster<MouseMotion>::connect;

    // ADD THESE!
    using EventBroadcaster<KeyPress>::signal;
    using EventBroadcaster<KeyPress>::connect;

    Input() {}
    virtual ~Input() {}
    void process()
    {
        MouseMotion mouseMotion;
        mouseMotion.x = 10;
        mouseMotion.y = 20;
        signal(mouseMotion);
    }
};

int main()
{
    Input input;

    int myX = 0;
    int myY = 0;

    // STORE THE LAMBDA IN A function<> OBJECT:
    std::function<void(const MouseMotion&)> onMouseMotion = [&](const MouseMotion& e){
        myX += e.x;
        myY += e.y;
    };

    // THIS WILL NOT BE AMBIGUOUS:
    input.connect(onMouseMotion);

    input.process();
    std::cout << myX << "," << myY << std::endl; // Output: 10,20

    return 0;
}

UPDATE:

What follows is an improvement of the above solution that does not require wrapping the lambda handler in an std::function object. It is enough to change the definition of the connect method as follows, and let SFINAE take care of ruling out the inappropriate overloads:

template<typename F>
void connect(F connection, decltype(std::declval<F>()(std::declval<Event>()))* = nullptr)
{
    connections.push_back(connection);
}

It is now possible to call connect() this way:

input.connect([&](const MouseMotion& e){
    myX += e.x;
    myY += e.y;
    });

input.connect([&](const KeyPress& e){
    std::cout << e.c;
    });
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • Wrapping the lambda in a function object does indeed remove the ambiguous compiler errors. Instead the compiler gives me the following errors: `no match for call to (std::function) (const KeyPress&)` and `no match for call to (std::function) (const MouseMotion&)`. Somehow mixing KeyPress and MouseMotion. – daniel Feb 22 '13 at 16:47
  • @Daniel: [this](http://liveworkspace.org/code/2CXJqe$0) works for me. Could you show the code you're trying? – Andy Prowl Feb 22 '13 at 16:54
  • Found the problem. I am passing Connection to connect using an rvalue reference, which seems to cause the errors. I should read some more about them before actually using them. :) – daniel Feb 22 '13 at 17:13
  • @Daniel: OK, glad it works. If this solves your problem, please consider accepting this answer. If it doesn't, let me know and I'll try to work on it a bit more :-) – Andy Prowl Feb 22 '13 at 17:40
  • I'd like to be able to call connect like `input.connect([&](const MouseMotion& e){myX += e.x;myY += e.y;});`. Using a std::function to wrap the lambda is not very friendly in use. I'm not really sure on how to do this. – daniel Feb 25 '13 at 13:06
  • @Daniel: What is your compiler? – Andy Prowl Feb 25 '13 at 15:08
  • @Daniel: I'm afraid GCC 4.7.2 has a bug that makes it hard to create an easy workaround. I have a solution that [works on Clang 3.2](http://liveworkspace.org/code/3SM5m1$8) (and on ICC 13.0.1 as well), but GCC throws a compilation error when I try it. I believe it is a bug. – Andy Prowl Feb 25 '13 at 15:17
  • @Daniel: I reworked the solution and this compiles with GCC 4.7.2 (see the update to the answer). Please check it out. – Andy Prowl Feb 25 '13 at 16:23