【译】C++ eventpp 使用教程:CallbackList、EventDispatcher、EventQueue 类

1,580 阅读5分钟

eventpp 是一个 header-only 、简洁易用的 C++ 事件库,能为 C++ 程序添加类似于 Qt 信号槽系统的事件机制。本专栏以该库文档的中文翻译为主,内容已贡献到该库的代码仓

源码仓库:github.com/wqking/even…

CallbackList 使用教程

注意:如果想尝试运行教程代码,建议使用 tests/unittest 目录下的代码。本文中的示例代码可能已经过期而无法编译。

CallbackList 教程 1, 基础

代码

// 命名空间是 eventpp
// 首个参数是监听器的原型
eventpp::CallbackList<void ()> callbackList;
​
// 添加一个回调函数,此处即 [](){} 。回调函数并非一定要是 lambda 表达式。
// 函数、std::function 或其他任何满足监听器原型要求的函数对象都可以作为监听器
callbackList.append([](){
    std::cout << "Got callback 1." << std::endl;
});
callbackList.append([](){
    std::cout << "Got callback 2." << std::endl;
});
​
// 启动回调列表
callbackList();

输出

Got callback 1.
Got callback 2.

解读

首先,定义一个回调列表( callback list )

eventpp::CallbackList<void ()> callbackList;

CallbackList 需要至少一个模板参数,作为回调函数的“原型”( prototype )。 “原型”指 C++ 函数类型,例如 void (int), void (const std::string &, const MyClass &, int, bool)

然后,添加一个回调函数

callbackList.append([]() {
    std::cout << "Got callback 1." << std::endl;
});

append 函数接收一个回调函数作为参数。 回调函数可以使任何回调目标——函数、函数指针、指向成员函数的指针、lambda 表达式、函数对象等。该回调函数必须可以使用 callbackList 中声明的原型调用。

接下来启动回调列表

callbackList();

在回调列表启动执行的过程中,所有回调函数都会按照被加入列表时的顺序执行。

CallbackList 教程 2, 带参数的回调函数

代码

// 下面这个 CallbackList 的回调函数原型有两个参数
eventpp::CallbackList<void (const std::string &, const bool)> callbackList;
​
callbackList.append([](const std::string & s, const bool b) {
    std::cout<<std::boolalpha<<"Got callback 1, s is " << s << " b is " << b << std::endl;
});
​
// 回调函数原型不需要和回调函数列表完全一致。只要参数类型兼容即可
callbackList.append([](std::string s, int b) {
    std::cout<<std::boolalpha<<"Got callback 2, s is " << s << " b is " << b << std::endl;
});
​
// 启动回调列表
callbackList("Hello world", true);

输出

Got callback 1, s is Hello world b is true
Got callback 2, s is Hello world b is 1

解读

本例中,回调函数列表的回调函数原型接收两个参数: const std::string &const bool。 回调函数的原型并不需要和回调完全一致,只要两个函数中的参数能够兼容即可。正如上面例子中的第二个回调函数,其参数为 [](std::string s, int b),其原型与回调列表中的并不相同。

CallbackList 教程 3, 移除

代码

using CL = eventpp::CallbackList<void ()>;
CL callbackList;
​
CL::Handle handle2;
​
// 加一些回调函数
callbackList.append([]() {
    std::cout << "Got callback 1." << std::endl;
});
handle2 = callbackList.append([]() {
    std::cout << "Got callback 2." << std::endl;
});
callbackList.append([]() {
    std::cout << "Got callback 3." << std::endl;
});
​
callbackList.remove(handler2);
​
// 启动回调列表。“Got callback 2.” 并不会被触发
callbackList();

输出

Got callback 1.
Got callback 3.

CallbackList 教程 4, for each

代码

using CL = eventpp::CallbackList<void ()>;
CL callbackList;
​
// 添加回调函数
callbackList.append([]() {
    std::cout << "Got callback 1." << std::endl;
});
callbackList.append([]() {
    std::cout << "Got callback 2." << std::endl;
});
callbackList.append([]() {
    std::cout << "Got callback 3." << std::endl;
});
​
// 下面调用 forEach 移除第二个回调函数
// forEach 回调函数的原型是
// void(const CallbackList::Handle & handle, const CallbackList::Callback & callback)
int index = 0;
callbackList.forEach([&callbackList, &index](const CL::Handle & handle, const CL::Callback & callback) {
    std::cout << "forEach(Handle, Callback), invoked " << index << std::endl;
    if(index == 1) {
        callbackList.remove(handle);
        std::cout << "forEach(Handle, Callback), removed second callback" << std::endl;
    }
    ++index;
});
​
// forEach 回调函数原型也可以是 void(const CallbackList::Callback & callback)
callbackList.forEach([&callbackList, &index](const CL::Callback & callback) {
    std::cout << "forEach(Callback), invoked" << std::endl;
});
​
// 启动回调列表。“Got callback 2.” 并不会被触发
callbackList();

输出

forEach(Handle, Callback), invoked 0
forEach(Handle, Callback), invoked 1
forEach(Handle, Callback), removed second callback
forEach(Handle, Callback), invoked 2
forEach(Callback), invoked
forEach(Callback), invoked
Got callback 1.
Got callback 3.

EventDispatcher 使用教程

注意:如果想尝试运行教程代码,建议使用 tests/unittest 目录下的代码。本文中的示例代码可能已经过期而无法编译。

教程 1 基本用法

代码

// 命名空间为 eventpp
// 第一个模板参数 int 是事件类型。事件类型可以是其他数据类型的,如 std::string,int 等
// 第二个参数是监听器的原型
eventpp::EventDispatcher<int, void ()> dispatcher;
​
// 添加一个监听器。这里的 3 和 5 是传给 dispatcher 的,用于标记自身的事件类型
// []() {} 是监听器。
// 监听器并不必须是 lambda,可以使任何满足原型要求的可调用对象,如函数、std::function等
dispatcher.appendListener(3, []() {
    std::cout << "Got event 3." << std::endl;
});
dispatcher.appendListener(5, []() {
    std::cout << "Got event 5." << std::endl;
});
dispatcher.appendListener(5, []() {
    std::cout << "Got another event 5." << std::endl;
});
​
// 分发事件。第一个参数是事件类型。
dispatcher.dispatch(3);
dispatcher.dispatch(5);

输出

Got event 3.
Got event 5.
Got another event 5.

解读

首先定义一个分发器

eventpp::EventDispatcher<int, void ()> dispatcher;

EventDispatcher 类接收两个模板参数。第一个是事件类型,此处是 int 。第二个是监听器的原型事件类型 必须能够用作 std::map 的 key。也就是说该类型必须支持 operator <原型 是 C++ 函数类型,例如 void (int), void (const std::string &, const MyClass &, int, bool)

然后添加一个监听器

dispatcher.appendListener(3, []() {
    std::cout << "Got event 3." << std::endl;
});

appendListener 函数接收两个参数。第一个是 事件类型事件 (译注:此处的“事件类型”指的是用于区分事件的数据类型,此处为 int 。“事件”则是具体的时间值,此处为整数 3 ),此处为 int 类型。第二个参数是回调函数。 回调函数可以是任何能够回调的目标——函数、函数指针、成员函数指针、lambda表达式、函数对象等。其必须能够被 dispatcher 中声明的 原型 调用。 在上面这段代码的下面,我们还为 事件5 添加了两个监听器。

接下来,使用下面的代码分发事件

dispatcher.dispatch(3);
dispatcher.dispatch(5);

这里分发了两个事件,分别是事件 3 和 5 。 在事件分发的过程中,所有对应事件的监听器都会按照它们被添加进 EventDispatcher 的顺序逐个执行。

教程 2 —— 带参数的监听器

代码

// 定义有两个参数的监听器原型
eventpp::EventDispatcher<int, void (const std::string &, const bool)> dispatcher;
​
dispatcher.appendListener(3, [](const std::string & s, const bool b) {
    std::cout << std::boolalpha << "Got event 3, s is " << s << " b is " << b << std::endl;
});
// 监听器的原型不需要和 dispatcher 完全一致,只要参数类型能够兼容即可
dispatcher.appendListener(5, [](std::string s, int b) {
    std::cout << std::boolalpha << "Got event 5, s is " << s << " b is " << b << std::endl;
});
dispatcher.appendListener(5, [](const std::string & s, const bool b) {
    std::cout << std::boolalpha << "Got another event 5, s is " << s << " b is " << b << std::endl;
});
​
// 分发事件。第一个参数是事件类型
dispatcher.dispatch(3, "Hello", true);
dispatcher.dispatch(5, "World", false);

输出

Got event 3, s is Hello b is true
Got event 5, s is World b is 0
Got another event 5, s is World b is false

解读

此处的 dispatcher 回调函数原型接收两个参数:const std::string &const bool。 监听器原型不需要和 dispatcher 完全一致,只要参数类型能够兼容即可。例如第二个监听器,[](std::string s, int b),其原型和 dispatcher 并不相同

教程 3 —— 自定义事件结构

代码

// 定义一个能够保存所有参数的 Event
struct MyEvent {
    int type;
    std::string message;
    int param;
};
​
// 定义一个能让 dispatcher 知道如何展开事件类型的 policy
struct MyEventPolicies
{
    static int getEvent(const MyEvent & e, bool /*b*/) {
        return e.type
    }
};
​
// 将刚刚定义的 MyEventPolicies 用作 EventDispatcher 的第三个模板参数
// 注意:第一个模板参数是事件类型的类型 int ,并非 MyEvent
eventpp::EventDispatcher<
    int,
    void (const MyEvent &, bool),
    MyEventPolicies
> dispatcher;
​
// 添加一个监听器。注意,第一个参数是事件类型 int,并非 MyEvent
dispatcher.appendListener(3, [](const MyEvent & e, bool b) {
    std::cout
        << std::boolalpha
        << "Got event 3" << std::endl
        << "Event::type is " << e.type << std::endl
        << "Event::message is " << e.message << std::endl
        << "Event::param is " << e.param << std::endl
        << "b is " << b << std::endl
    ;
});
​
// 启动事件。第一个参数是 Event
dispatcher.dispatch(MyEvent { 3, "Hello world", 38 }, true);

输出

Got event 3
Event::type is 3
Event::message is Hello world
Event::param is 38
b is true

解读

通常的方法是将 Event 类定义为基类,所有其他的事件都从 Event 派生,实际的事件类型则是 Event 的成员(就像 Qt 中的 QEvent ),通过 policy 来为 EventDispatcher 定义如何从 Event 类中获取真正需要的数据。

EventQueue 使用教程

注意:如果想尝试运行教程代码,建议使用 tests/unittest 目录下的代码。本文中的示例代码可能已经过期而无法编译。

教程 1 基本用法

代码

eventpp::EventQueue<int, void (const std::string &, std::unique_ptr<int> &)> queue;
​
queue.appendListener(3, [](const std::string & s, std::unique_ptr<int> & n) {
    std::cout << "Got event 3, s is " << s << " n is " << *n << std::endl;
});
​
// 监听器原型不需要和 dispatcher 完全一致,参数类型兼容即可
queue.appendListener(5, [](std::string s, const std::unique_ptr<int> & n) {
    std::cout << "Got event 5, s is " << s << " n is " << *n << std::endl;
});
queue.appendListener(5, [](const std::string & s, std::unique_ptr<int> & n) {
    std::cout << "Got another event 5, s is " << s << " n is " << *n << std::endl;
});
​
// 将事件加入队列,首个参数是事件类型。监听器在入队列期间不会被触发
queue.enqueue(3, "Hello", std::unique_ptr<int>(new int(38)));
queue.enqueue(5, "World", std::unique_ptr<int>(new int(58)));
​
// 处理事件队列,分发队列中的所有事件
queue.process();

输出

Got event 3, s is Hello n is 38
Got event 5, s is World n is 58
Got another event 5, s is World n is 58

解读 EventDispatcher<>::dispatch() 触发监听器的动作是同步的。但异步事件队列在某些场景下能发挥更大的作用(例如 Windows 消息队列、游戏中的消息队列等)。EventQueue 就是用于满足该类需求的事件队列。 EventQueue<>::enqueue() 将事件加入队列,其参数和 dispatch 的参数完全相同。 EventQueue<>::process() 用于分发队列中的事件。不调用 process ,事件就不会被分发。 事件队列的典型用例:在 GUI 应用中,每个组件都调用 EventQueue<>::enqueue() 来发布事件,然后主事件循环调用 EventQueue<>()::process() 来 dispatch 所有队列中的事件。 EventQueue 支持将不可拷贝对象作为事件参数,例如上面例子中的 unique_ptr

教程 2 —— 多线程

代码

using EQ = eventpp::EventQueue<int, void (int)>;
EQ queue;
​
constexpr int stopEvent = 1;
constexpr int otherEvent = 2;
​
// 启动一个新线程来处理事件队列。所有监听器都会在该线程中启动运行
std::thread thread([stopEvent, otherEvent, &queue]() {
    volatile bool shouldStop = false;
    queue.appendListener(stopEvent, [&shouldStop](int) {
        shouldStop = true;
    });
    queue.appendListener(otherEvent, [](const int index) {
        std::cout << "Got event, index is " << index << std::endl;
    });
    
    while(! shouldStop) {
        queue.wait();
        
        queue.process();
    }
});
​
// 将一个主线程的事件加入队列。在休眠 10 ms 时,该事件应该已经被另一个线程处理了
queue.enqueue(otherEvent, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Should have triggered event with index = 1" << std::endl;
​
queue.enqueue(otherEvent, 2);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Should have triggered event with index = 2" << std::endl;
​
{
    // EventQueue::DisableQueueNotify 是一个 RAII 类,能避免唤醒其他的等待线程。
    // 所以该代码块内不会触发任何事件。
    // 当需要一次性添加很多事件,希望在事件都添加完成后才唤醒等待线程时,
    // 就可以使用 DisableQueueNotify 
    EQ::DisableQueueNotify disableNotify(&queue);
    
    queue.enqueue(otherEvent, 10);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::cout << "Should NOT trigger event with index = 10" << std::endl;
    
    queue.enqueue(otherEvent, 11);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::cout << "Should NOT trigger event with index = 11" << std::endl;
}
// DisableQueueNotify 对象在此处销毁,恢复唤醒其他的等待线程。因此事件都会在此处触发
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Should have triggered events with index = 10 and 11" << std::endl;
​
queue.enqueue(stopEvent, 1);
thread.join();

输出

Got event, index is 1
Should have triggered event with index = 1
Got event, index is 2
Should have triggered event with index = 2
Should NOT trigger event with index = 10
Should NOT trigger event with index = 11
Got event, index is 10
Got event, index is 11
Should have triggered events with index = 10 and 11