1

我最近一直在玩新的 C++11 标准,并决定创建一个基本的事件处理系统。下面的代码提供了我当前实现的一个小例子。

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

Input如果该类只广播单个事件,上述解决方案确实可以很好地工作。然而,可能存在Input类希望能够发送KeyPress事件而不仅仅是MouseMotion事件的情况。

我考虑过使用多重继承。同时Input继承EventBroadcaster<MouseMotion>EventBroadcaster<KeyPress>。这会导致编译器错误警告不明确的函数。以下答案多重继承模板类中提供的解决方案确实适用于受保护的signal函数,但不适用于在类外调用的公共connect函数Input

除了多重继承,我想知道可变参数模板是否可以帮助我解决问题。我已经查看了(部分)模板专业化和解包可变参数模板。但是一直无法提供(优雅的)解决方案。

支持多种事件类型的最佳方式是什么?

4

2 回答 2

4

我会做EventBroadcaster<T>一个实现细节,而不是公开EventBroadcasters<Ts...>.

EventBroadcasters<Ts...>拥有EventBroadcaster<Ts>...模板方法connect<U>signal<U>,并将这些转发给EventBroadcaster<U>. 如果U不在Ts....

所以现在你signal(mouseMotion),它解析为signal<MouseMotion>(mouseMotion),它连接到正确的广播公司。

当你connect倾听时,同样只要你使用正确的std::function东西就可以了。如果没有,您可以传入Event您正在收听的类型。

有一些方法可以让它变得更加神奇,并启用你想要的完全正确的重载解决方案,但这真的很棘手。即,您检测connect到执行 SFINAE 并手动将传入的 lambda(通过检查其operator())分派到正确的std::function覆盖。不过,单纯的能说connect<MouseMotion>([&](MouseMotion m){...})应该是一种进步。

(SFINAE 技术包括检查std::function列表中的哪种类型可以从您传入的 中构造U,如果有一个唯一的,使用那个。另外,检查是否U是这样的std::function(或此类的 cv 变体)。它不是很难,但看起来很凌乱,根据我的经验,大多数人不喜欢打字,更喜欢只指定<MouseMotion>。)

更新:

以下代码块提供了此答案的实现。

#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;
}
于 2013-02-22T16:43:18.920 回答
1

如果这对您来说可以,您可以通过在您的类中添加using指令并将您的 lambda 存储在一个对象中而不是使用来解决这个问题:KeyPressInputstd::function<>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;
}

更新:

接下来是对上述解决方案的改进,它不需要将 lambda 处理程序包装在std::function对象中。如下更改connect方法的定义就足够了,让 SFINAE 负责排除不适当的重载:

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

现在可以这样调用connect()

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

input.connect([&](const KeyPress& e){
    std::cout << e.c;
    });
于 2013-02-22T16:32:05.290 回答