使用模板的事件处理

Event-handling using templates

本文关键字:事件处理      更新时间:2023-10-16

我最近一直在尝试新的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类希望能够发送除了MouseMotion事件之外的KeyPress事件的情况。

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

除了多重继承,我想知道可变模板是否可以帮助我解决这个问题。我已经研究了(部分(模板专业化和拆包可变模板。但一直未能找到一个(优雅的(解决方案。

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

我会将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;
}

如果可以,您可以通过在Input类中添加KeyPressusing指令来解决此问题,并将lambda存储在std::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;
    });