我最近一直在尝试新的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
类中添加KeyPress
的using
指令来解决此问题,并将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;
});