CppEvent - 如何使用标准 C++ 实现事件





5.00/5 (26投票s)
本文展示了如何使用标准 C++ 库实现一个线程安全的事件(类似于 .NET 事件)机制。
目录
介绍
在开发应用程序时,我们有时需要一种机制,以便在发生某些事情时执行某些任务。为此,我们通常希望将所需的任务注册到某个地方,并在收到相关事件的通知时调用它们。一些框架已经内置了满足此需求的解决方案(例如:.NET 事件、Qt 信号)。但在我看来,我不得不仅使用标准 C++ 来实现这种行为。由于我发现每次想要实现它时都会重复相同的模式,我认为拥有一个通用解决方案来满足这种常见需求会很好。
本文展示了如何使用标准 C++ 库实现一个线程安全的解决方案,该方案可以以类似于 .NET 事件的方式使用。
背景
由于我们的解决方案的使用方式与 .NET 事件类似,我们使用相同的术语。
- 事件处理程序:一个保存实际函数的地方,当发出通知时应调用该函数。
- 事件:一个保存多个处理程序的容器。可以调用一个事件来发出通知(并调用其处理程序)。
本文假设读者对 C++ 语言和标准 C++ 库有基本了解。
工作原理
保存处理函数
在我们的解决方案中,我们必须提供一种为不同事件定义事件处理程序的方法。由于任何事件处理函数的签名都可能不同,我们需要一种方法来为任何事件类型定义不同的参数。为了实现这一目标,我们创建了一个变长类模板。变长模板是其参数可以在模板的实例之间变化的模板。为了实现这一点,C++11 为我们带来了参数包。使用省略号运算符,我们可以声明和展开我们的变长模板的参数。
对于我们的模板,我们使用参数包来定义处理函数参数的类型。
template <typename... Args> class event_handler
{
};
为了保存事件处理函数的函数,我们使用std::function 对象。std::function
的定义由一个未定义类模板(接受一个模板参数)和一个偏特化模板(接受一个用于函数返回类型的模板参数和一个用于函数参数类型的参数包)组成。单个模板参数被定义为一个函数类型,使用函数的返回类型和函数的参数类型。
在我们的情况下,函数的返回类型始终是void
。使用void
作为函数的返回类型,并将模板的参数包作为函数的参数类型,我们可以定义我们的处理程序函数容器。
typedef std::function<void(Args...)> handler_func_type;
handler_func_type m_handlerFunc;
为了调用我们的函数,我们添加了一个合适的函数调用运算符。
void operator()(Args... params) const
{
if (m_handlerFunc)
{
m_handlerFunc(params...);
}
}
由于一个事件可以保存一些事件处理程序,我们需要一种方法来识别每个事件处理程序。为此,我们添加了一个数据成员来保存处理程序的标识号。为了使其线程安全,我们使用原子类型。
template <typename... Args> class event_handler
{
public:
// ...
typedef unsigned int handler_id_type;
explicit event_handler(const handler_func_type& handlerFunc)
: m_handlerFunc(handlerFunc)
{
m_handlerId = ++m_handlerIdCounter;
}
bool operator==(const event_handler& other) const
{
return m_handlerId == other.m_handlerId;
}
handler_id_type id() const
{
return m_handlerId;
}
private:
handler_id_type m_handlerId;
static std::atomic_uint m_handlerIdCounter;
};
template <typename... Args> std::atomic_uint event_handler<Args...>::m_handlerIdCounter(0);
集中保存一些事件处理程序
通常,在使用事件时,我们希望发布一个关于某事发生的通知,并让一些订阅者用他们需要的实现进行订阅。为此,我们需要一种方法将一些事件处理程序组合在一起,并在某事(事件)发生时调用所有这些处理程序。我们可以通过添加另一个变长类模板来实现这一点。
template <typename... Args> class event
{
public:
typedef event_handler<Args...> handler_type;
protected:
typedef std::list<handler_type> handler_collection_type;
private:
handler_collection_type m_handlers;
};
在event
类中,我们保存了一个事件处理程序对象的集合。由于我们希望我们的事件是线程安全的,我们使用互斥锁来保护集合的操作。
typename handler_type::handler_id_type add(const handler_type& handler)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
m_handlers.push_back(handler);
return handler.id();
}
inline typename handler_type::handler_id_type add
(const typename handler_type::handler_func_type& handler)
{
return add(handler_type(handler));
}
bool remove(const handler_type& handler)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
auto it = std::find(m_handlers.begin(), m_handlers.end(), handler);
if (it != m_handlers.end())
{
m_handlers.erase(it);
return true;
}
return false;
}
bool remove_id(const typename handler_type::handler_id_type& handlerId)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
auto it = std::find_if(m_handlers.begin(), m_handlers.end(),
[handlerId](const handler_type& handler) { return handler.id() == handlerId; });
if (it != m_handlers.end())
{
m_handlers.erase(it);
return true;
}
return false;
}
mutable std::mutex m_handlersLocker;
调用事件处理程序
在有了我们的事件类之后,我们可以添加一个函数来调用其事件处理程序。由于我们的事件可以在多个线程中同时使用,并且我们不希望阻止其他线程使用该事件(添加和删除处理程序、调用事件)直到所有事件处理程序完成其实现,因此我们仅锁定互斥锁以获取事件处理程序的副本。然后,我们遍历复制的处理程序并无锁地调用它们。这可以通过以下方式完成:
void call(Args... params) const
{
handler_collection_type handlersCopy = get_handlers_copy();
call_impl(handlersCopy, params...);
}
void call_impl(const handler_collection_type& handlers, Args... params) const
{
for (const auto& handler : handlers)
{
handler(params...);
}
}
handler_collection_type get_handlers_copy() const
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
// Since the function return value is by copy,
// before the function returns (and destruct the lock_guard object),
// it creates a copy of the m_handlers container.
return m_handlers;
}
为了使我们的事件更方便(更像 C# 事件),我们用适当的运算符包装了add
、remove
和call
函数。
inline void operator()(Args... params) const
{
call(params...);
}
inline typename handler_type::handler_id_type operator+=(const handler_type& handler)
{
return add(handler);
}
inline typename handler_type::handler_id_type
operator+=(const typename handler_type::handler_func_type& handler)
{
return add(handler);
}
inline bool operator-=(const handler_type& handler)
{
return remove(handler);
}
有时,我们不想等待所有事件处理程序都完成。我们只想触发一个事件然后继续。为此,我们添加了另一个函数来异步调用我们的事件处理程序。
std::future<void> call_async(Args... params) const
{
return std::async(std::launch::async, [this](Args... asyncParams)
{ call(asyncParams...); }, params...);
}
最后,为了使我们的事件可复制并可移动,我们添加了适当的复制构造函数、复制赋值运算符、移动构造函数和移动赋值运算符。
template <typename... Args> class event_handler
{
// ...
// copy constructor
event_handler(const event_handler& src)
: m_handlerFunc(src.m_handlerFunc), m_handlerId(src.m_handlerId)
{
}
// move constructor
event_handler(event_handler&& src)
: m_handlerFunc(std::move(src.m_handlerFunc)), m_handlerId(src.m_handlerId)
{
}
// copy assignment operator
event_handler& operator=(const event_handler& src)
{
m_handlerFunc = src.m_handlerFunc;
m_handlerId = src.m_handlerId;
return *this;
}
// move assignment operator
event_handler& operator=(event_handler&& src)
{
std::swap(m_handlerFunc, src.m_handlerFunc);
m_handlerId = src.m_handlerId;
return *this;
}
};
template <typename... Args> class event
{
// ...
// copy constructor
event(const event& src)
{
std::lock_guard<std::mutex> lock(src.m_handlersLocker);
m_handlers = src.m_handlers;
}
// move constructor
event(event&& src)
{
std::lock_guard<std::mutex> lock(src.m_handlersLocker);
m_handlers = std::move(src.m_handlers);
}
// copy assignment operator
event& operator=(const event& src)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
std::lock_guard<std::mutex> lock2(src.m_handlersLocker);
m_handlers = src.m_handlers;
return *this;
}
// move assignment operator
event& operator=(event&& src)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
std::lock_guard<std::mutex> lock2(src.m_handlersLocker);
std::swap(m_handlers, src.m_handlers);
return *this;
}
};
如何使用
演示辅助函数和环境设置
在 Eclipse 项目中支持 C++11 和线程
为了开发我们的示例(在 Linux 下),我们使用 Eclipse 环境。由于我们在代码中使用了 C++11,因此我们必须在项目中支持它。我们可以通过在项目属性的编译器设置中添加-std=c++11
来实现此目的。
为了让 Eclipse 索引识别 c++11,在 GNU C++ 符号(在项目属性中)中,我们可以:
- 添加
__GXX_EXPERIMENTAL_CXX0X__
符号(值为空) - 将
__cplusplus
符号的值更改为201103L
(或更大的值)。
由于我们在项目中使用线程,我们将pthread
添加到项目链接库中。
用于彩色控制台打印的辅助类
由于我们在示例中有一些彩色控制台打印,我们添加了一个辅助类来简化此操作。
template <typename ItemT, typename CharT> class colored
{
};
colored
类接受两个模板参数。第一个用于打印项的类型,第二个用于输出流的字符类型。
尽管在我们的示例中,我们仅使用此辅助类来打印字符串和数字,但它被实现为对其他复杂类型也更有效。因此,由于我们colored
类的使用仅用于临时控制台打印,我们可以限制其仅用于临时对象。
template <typename ItemT, typename CharT> class colored
{
public:
colored(const CharT* colorStr, const ItemT& item)
: m_colorStr(colorStr), m_item(item)
{
}
// Disable Copy
colored(const colored& other) = delete;
colored& operator=(const colored& other) = delete;
// Enable Move
colored(colored&& other)
: m_colorStr(other.m_colorStr), m_item(other.m_item)
{
}
// The parameter is an R-Value reference since we don't want retained copies of this class.
friend std::basic_ostream<CharT>& operator<<(std::basic_ostream<CharT>& os, colored&& item)
{
static const CharT strPrefix[3]{ '\x1b', '[', '\0' };
static const CharT strSuffix[5]{ '\x1b', '[', '0', 'm', '\0' };
os << strPrefix << item.m_colorStr << CharT('m') << item.m_item << strSuffix;
return os;
}
private:
const CharT* m_colorStr;
const ItemT& m_item;
};
在colored
类的构造函数中,我们存储了给定项的引用(而不是复制它),我们不允许对此类进行复制操作(仅移动),并且我们实现了<<
运算符以仅接受右值引用。
由于类模板参数推导仅在 C++17 中受支持,我们为早期 C++ 版本提供了一个辅助函数。
template <typename ItemT, typename CharT> colored<ItemT, CharT>
with_color(const CharT* colorStr, const ItemT& item)
{
return colored<ItemT, CharT>(colorStr, item);
}
使用此函数,我们可以使用我们的colored
类而无需指定模板参数(它们是通过函数模板参数推导推导出来的)。
支持 Windows 10 控制台的虚拟终端序列
我们还可以通过以下方式在 Windows 10 示例中支持控制台虚拟终端序列:
#include <windows.h>
// Define ENABLE_VIRTUAL_TERMINAL_PROCESSING, if it isn't defined.
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif
DWORD InitializeWindowsEscSequnces()
{
// Set output mode to handle virtual terminal sequences
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE)
{
return GetLastError();
}
DWORD dwMode = 0;
if (!GetConsoleMode(hOut, &dwMode))
{
return GetLastError();
}
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if (!SetConsoleMode(hOut, dwMode))
{
return GetLastError();
}
return 0;
}
有关此主题的更多信息,您可以访问此处。
演示应用程序
为了演示我们的event
类,我们创建了一个发布者类来发布一些事件。
class EventsPublisher
{
};
在此类中,我们添加了:
- 一个我们将通过手动请求发布的事件。
// event of <message, publisher id>. sz::event<const std::string&, int> SomethingHappened;
- 一个将通过计时器发布的事件。
sz::event<unsigned int> TimerTick;
为了实现我们的计时器,我们添加了一个新类。
class MyTimer
{
public:
MyTimer();
~MyTimer();
sz::event<> Tick;
bool Start(unsigned int millisecondsInterval);
bool Stop();
private:
void TimerFunc();
bool m_isRunning;
unsigned int m_millisecondsInterval;
std::thread m_timerThread;
};
// Can be implemented to be more thread safe. But, it's only for the example.
// If the interval is too big, the Stop function will be affected too.
#define DEFAULT_TIMER_INTERVAL 1000
MyTimer::MyTimer()
: m_isRunning(false), m_millisecondsInterval(DEFAULT_TIMER_INTERVAL)
{
}
MyTimer::~MyTimer()
{
}
bool MyTimer::Start(unsigned int millisecondsInterval)
{
if (m_isRunning)
{
return false;
}
m_isRunning = true;
m_millisecondsInterval = millisecondsInterval > 0 ? millisecondsInterval : DEFAULT_TIMER_INTERVAL;
m_timerThread = std::thread([this]() { TimerFunc(); });
return true;
}
bool MyTimer::Stop()
{
if (!m_isRunning)
{
return false;
}
m_isRunning = false;
if (m_timerThread.joinable())
{
m_timerThread.join();
}
return true;
}
void MyTimer::TimerFunc()
{
while (m_isRunning)
{
std::this_thread::sleep_for(std::chrono::milliseconds(m_millisecondsInterval));
if (m_isRunning)
{
Tick();
}
}
}
在MyTimer
类中,我们实现了一个计时器,该计时器运行一个线程,该线程以恒定间隔发布一个Tick
事件。
在EventsPublisher
类中,我们订阅Tick
事件,并以当前滴答数为参数发布TimerTick
事件。
class EventsPublisher
{
// ...
MyTimer m_timer;
unsigned int m_counter;
};
EventsPublisher::EventsPublisher()
: m_counter(0)
{
m_timer.Tick += [this]() {
m_counter++;
TimerTick(m_counter);
};
}
在有了EventsPublisher
类之后,我们可以向其注册一些事件处理程序。
EventsPublisher ep;
std::mutex printLocker;
sz::event_handler<unsigned int> timerHandler1([&ep, &printLocker](unsigned int counter) {
if ((counter % 5) == 0)
{
ep.SomethingHappened.call_async("Something happened from timer handler 1", 1);
}
std::lock_guard<std::mutex> lock(printLocker);
std::cout << sz::with_color("31", "Timer handler1: Timer tick ")
<< sz::with_color("41;97", counter) << std::endl;
});
sz::event_handler<unsigned int> timerHandler2([&ep, &printLocker](unsigned int counter) {
if ((counter % 7) == 0)
{
ep.SomethingHappened.call_async("Something happened from timer handler 2", 2);
}
std::lock_guard<std::mutex> lock(printLocker);
std::cout << sz::with_color("32", "Timer handler2: Timer tick ")
<< sz::with_color("42;97", counter) << std::endl;
});
// We can create an event_handler also for this handler.
// But, we want to demonstrate the use without it.
auto somethingHappenedHandlerId = ep.SomethingHappened.add(
[&printLocker](const std::string& message, int publisherId) {
std::lock_guard<std::mutex> lock(printLocker);
std::cout << "Something happened. Message: "
<< sz::with_color(publisherId == 1 ? "91" : "92", message.c_str())
<< std::endl;
});
ep.TimerTick += timerHandler1;
ep.TimerTick += timerHandler2;
在TimerTick
事件的事件处理程序中,我们打印一条消息以指示事件处理,并为每几个滴答数异步发布一个SomethingHappened
事件。
在SomethingHappened
事件的事件处理程序中,我们打印收到的消息。
注册了所需的事件处理程序后,我们启动EventsPublisher
并等待用户停止演示。
std::cout << sz::with_color("93", "Press <Enter> to stop.") << std::endl;
ep.Start();
getchar();
ep.SomethingHappened.remove_id(somethingHappenedHandlerId);
ep.TimerTick -= timerHandler1;
ep.TimerTick -= timerHandler2;
ep.Stop();
结果是: