【译】eventpp 的策略(policies)与混入(mixins)

345 阅读11分钟

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

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

策略(Policies)

目录

介绍

eventpp 使用了基于策略的设计来配置和扩展各个组件的行为。EventDispatcher、EventQueue、CallbackList 的最后一个模板参数就是策略类。这三个类都有名为 DefaultPolicies 的默认策略类。

一项策略可以是策略类中的一个类型或一个静态成员函数。所有策略都必须是 public 的,所以通常可以用 struct 来定义策略类。

所有策略都是可选的。若省略了一个策略,那么这项策略就将使用其默认值。实际上,DefaultPolicies 本身就是一个空结构体。

EventDispatcher、EventQueue、CallbackList 这三个类使用了相同的策略机制,尽管不是所有的类都需要相同的策略。

策略

函数: getEvent

原型static EventKey getEvent(const Args &...) 。该函数接收与 EventDispatcher::dispatchEventQueue::enqueue 相同的参数,且必须返回一个事件类型。
默认值:默认实现是返回 getEvent 的第一个实参。
适用于:EventDispatcher, EventQueue

evetpp 将 EventDispatcher::dispatchEventQueue::enqueue 的所有实参(这两个函数的参数相同)都转发给 getEvent 以获取事件类型,然后再触发执行该事件对应的回调函数列表。
getEvnet 既可以是模板函数,也可以是非模板函数。只要 getEvent 能够使用与 EventDispatcher::dispatchEventQueue::enqueue 相同的参数调用即可。

示例代码如下:

// 定义用于保存所有参数的事件结构
struct MyEvent {
    int type;
    std::string message;
    int param;
};

// 为调度器定义如何分解事件类型的策略
struct MyEventPolicies
{
    static int getEvent(const MyEvent & e, bool /*b*/) {
        return e.type;
    }
};

// 将 MyEventPolicies 作为 EventDispatcher 的第三个模板参数
// 注意:第一个模板参数是整型的事件类型,而非 MyEvent。
eventpp::EventDispatcher<
    int,
    void (const MyEvent &, bool),
    MyEventPolicies
> dispatcher;

// 添加一个监听器。
// 注意:第一个参数是整型的事件类型,而非 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);

函数: canContinueInvoking

原型static bool canContinueInvoking(const Args &...) 。该函数接收与 EventDispatcher::dispatchEventQueue::enqueue 相同的参数,且必须在事件调度或回调列表调用可以继续进行的时候返回 true ,在调用需要停止的时候返回 false 。
默认值:默认实现总是返回 true 。
适用于:CallbackList, EventDispatcher, EventQueue

示例代码如下:

struct MyEvent {
    MyEvent() : type(0), canceled(false) {
    }
    explicit MyEvent(const int type)
        : type(type), canceled(false) {
    }

    int type;
    mutable bool canceled;
};

struct MyEventPolicies
{
    static int getEvent(const MyEvent & e) {
        return e.type;
    }

    static bool canContinueInvoking(const MyEvent & e) {
        return ! e.canceled;
    }
};

eventpp::EventDispatcher<int, void (const MyEvent &), MyEventPolicies> dispatcher;

dispatcher.appendListener(3, [](const MyEvent & e) {
    std::cout << "Got event 3" << std::endl;
    e.canceled = true;
});
dispatcher.appendListener(3, [](const MyEvent & e) {
    std::cout << "Should not get this event 3" << std::endl;
});

dispatcher.dispatch(MyEvent(3));

类型: Mixins

默认值using Mixins = eventpp::MixinList<> 。未启用任何 mixin 。
适用于:CallbackList, EventDispatcher, EventQueue

Mixin 用于向 EventDispatcher / EventQueue 继承层次中注入代码,以扩展它们的功能。更多细节请参阅 github.com/wqking/even…

类型: Callback

默认值using Callback = std::function<Parameters of callback>
适用于:CallbackList, EventDispatcher, EventQueue

Callback 是用于在底层维护回调函数。默认是 std::function

类型: Threading

默认值using Threading = eventpp::MultipleThreading
适用于:CallbackList, EventDispatcher, EventQueue, HeterCallbackList, HeterEventDispatcher, HeterEventQueue.

Threading 控制线程执行模型。默认是“多线程”( Multiple Threading )。可取值:

  • MultipleThreading : 使用 mutex 保护核心数据。该选项为默认值。
  • SingleThreading : 不保护核心数据,且这些数据无法从其他线程访问

一个典型的 Threading 类型如下:

struct MultipleThreading
{
    using Mutex = std::mutex;
    
    template <typename T>
    using Atomic = std::atomic<T>;
    
    using ConditionVariable = std::condition_variable;
};

对于 SingleThreading 而言,所有的 MutexAtomicConditionVariable 类型都是不会起任何作用的假类型。
对于多线程而言,默认的 Mutexstd::mutex 。 eventpp 也提供了一个使用自旋锁作为互斥量的 SpinLock 类。
当只有较少的线程时(和 CPU 核心数差不多的线程数),eventpp::SpinLock 的性能比 std::mutex 更高一些。当线程数超过 CPU 核心数时, eventpp::SpinLock 的性能弱于 std::mutex

基准测试相关数据请参阅:github.com/wqking/even…

下面是使用 SpinLock 的示例代码:

struct MultipleThreadingSpinLock
{
	using Mutex = eventpp::SpinLock;
    
    template <typename T>
    using Atomic = std::atomic<T>;
    
    using ConditionVariable = std::condition_variable;
};
struct MyEventPolicies {
    using Threading = MultipleThreadingSpinLock;
};
eventpp::EventDispatcher<int, void (), MyEventPolicies> dispatcher;
eventpp::CallbackList<void (), MyEventPolicies> callbackList;

eventpp 还提供了一个简化易用的自定义线程管理的模板类。

template <
	typename Mutex_,
	template <typename > class Atomic_ = sd::atomic,
    typename ConditionVariable_ = std::condition_variable
>
struct GeneralThreading
{
    using Mutex = Mutex_;
    
    template <typename T>
    using Atomic = Atomic_<T>;
    
    using ConditionVariable = ConditionVariable_;
};

因此前面自旋锁的示例代码可以重写为

struct MyEventPolicies {
    using Threading = eventpp::GeneralThreading<eventpp::SpinLock>;
};
eventpp::EventDispatcher<int, void (), MyEventPolicies> dispatcher;
eventpp::CallbackList<void (), MyEventPolicies> callbackList;

类型: ArgumentPassingMode

默认值using ArgumentPassingMode = ArgumentPassingAutoDetect
适用于:EventDispatcher, EventQueue

ArgumentPassingMode 是实参传递的模式。默认值是 ArgumentPassingAutoDetect

示例代码如下。假设我们有一个调度器

eventpp::EventDispatcher<int, void(int, const std::string &)> dispatcher;

事件类型是 int
监听器的第一个参数也是 int 。根据具体的事件调度方式,监听器的第一个参数可以是事件类型,也可以是一个额外的参数。

dispatcher.dispatch(3, "hello");

事件 3 会被使用一个参数 "hello" 调度,监听器将会被使用参数 (3, "hello") 触发执行,第一个参数是事件类型。

dispatcher.dispatch(3, 8, "hello");

事件 3 会被使用两个参数 8 和 "hello" 调度,监听器将会被使用参数 (8, "hello") 触发执行,第一个参数就是额外参数,此时的事件类型参数将被忽略。
因此,在默认情况下,EventDispatcher 会自动监测 dispatch 的参数数量和监听器原型,以决定是否使用事件类型来调用监听器。

默认规则简便、宽松但容易出错。可以通过 ArgumentPassingMode 策略控制具体的行为

struct ArgumentPassingAutoDetect;
struct ArgumentPassingIncludeEvent;
struct ArgumentPassingExcludeEvent;

ArgumentPassingAutoDetect:默认策略。自动检查是否要传递事件类型。
ArgumentPassingIncludeEvent:总是传递事件类型。参数数量不符会导致编译失败。
ArgumentPassingExcludeEvent:总是忽略且不会传递事件类型。参数数量不符会导致编译失败。

假设监听器原型有 P 个参数,dispatch 中的参数数量(包括事件类型在内)为 D ,则 P 和 D 的关系为:
对于 ArgumentPassingAutoDetect:P == D 或 P + 1 == D
对于 ArgumentPassingIncludeEvent:P == D
对于 ArgumentPassingExcludeEvent: P + 1 == D

注意:同样的规则也适用于 EventQueue::enqueue ,因为 enqueue 的参数与 dispatch 相同。

参数传递模式的示例代码如下:

struct MyPolicies
{
    using ArgumentPassingMode = ArgumentPassingAutoDetect;
};
eventpp::EventDispatcher<
    int,
    void(int, const std::string &),
    MyPolicies
> dispatcher;
// 或用下面的简便写法
//eventpp::EventDispatcher<int, void(int, const std::string &)> dispatcher;
dispatcher.dispatch(3, "hello"); // 编译通过
dispatcher.dispatch(3, 8, "hello"); // 编译通过
struct MyPolicies
{
    using ArgumentPassingMode = ArgumentPassingIncludeEvent;
};
eventpp::EventDispatcher<
    int,
    void(int, const std::string &),
    MyPolicies
> dispatcher;
dispatcher.dispatch(3, "hello"); // 编译通过
//dispatcher.dispatch(3, 8, "hello"); // 编译失败
struct MyPolicies
{
    using ArgumentPassingMode = ArgumentPassingExcludeEvent;
};
eventpp::EventDispatcher<
    int,
    void(int, const std::string &),
    MyPolicies
> dispatcher;
//dispatcher.dispatch(3, "hello"); // 编译失败
dispatcher.dispatch(3, 8, "hello"); // 编译通过

模板: Map

原型

template <typename Key, typename T>
using Map = // std::map <Key, T> 或其他 map 类型

默认值:自动确定
应用于:EventDispatcher, EventQueue

Map 是 EventDispatcher 和 EventQueue 用于维护底层键值对(事件类型,CallbackList)的关联容器类型。

Map 是有两个参数的模板,两个参数分别是键和值。

Map 必须能够支持 []find()end() 操作。

若没有指定 Map ,eventpp 会自动确定类型。若事件类型支持 std::hash 会使用 std::unordered_map ,否则会使用 std::map

模板: QueueList

原型

template <typename Item>
using QueueList = std::list<Item>;

默认值std::list
应用于:EventQueue

QueueList 用于管理 EventQueue 内部的事件,作为队列使用。事件会被追加到 QueueList 的末尾,当被处理时,事件会从 QueueList 的头部弹出。

使用不同的 QueueList 能够更好地控制队列。例如,若 QueueList 能够管理事件的顺序,那么队列中的事件就能以不同于加入顺序的新顺序被处理。

一个 QueueList 不需要实现 std::list 的所有成员,其只需要实现下面的类型和函数即可:

type iterator;
type const_iterator;
bool empty() const;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
const_reference front() const;
void swap(QueueList & other);
void emplace_back();
void splice(const_iterator pos, QueueList & other );
void splice(const_iterator pos, QueueList & other, const_iterator it);

eventpp 中的有序队列列表 OrderedQueueList 就是一个应用实例。详细内容请阅 github.com/wqking/even…

如何使用策略

想要使用策略,只需要声明一个结构体,在其中定义策略然后传递给 CallbackList, EventDispatcher 或 EventQueue 即可。

struct MyPolicies // 结构体的名字并不重要
{
    template <typename ...Args>
    static int getEvent(const MyEvent & e, const Args &...) {
        return e.type;
    }
};
EventDispatcher<int, void(const MyEvent &), MyPolicies> dispatcher;

上面的示例代码展示了一个重定义了 getEvent 策略类,除该策略外的其他策略都保持默认值。

Mixins

目录

介绍

Mixin 用于向 EventDispatcher / EventQueue 继承层次中注入代码,以扩展它们的功能。本文将以 EventDispatcher 为例进行介绍,EventQueue 的 mixin 相关用法与 EventDispatcher 完全一致。

原有的 EventDispatcher 继承层次如下:

EventDispatcher <- EventDispatcherBase

EventDispatcher 是一个空类,其继承来自 EventDispatcherBase 的所有函数和数据。

在注入两个 mixin 类 A 、 B 后,层次如下:

EventDispatcher <- MixinA <- MixinB <- EventDispatcherBase

mixin 能够使用所有 EventDispatcherBase 中的所有 public 和 protected 成员(类型、函数和数据)。mixin 中的所有公共成员对用户来说都是可见、可用的。

定义一个 mixin

一个 mixin 是有着一个模板参数的模板类。mixin 必须继承其模板参数。一个典型的 mixin 如下:

template <typename Base>
class MyMixin : public Base
{  
};

将 mixin 注入到 EventDispatcher

想要启用 mixin ,需要将 mixin 加入到策略类的 Mixins 类型中。例如,要启动 MixinFilter ,可以像下面这样来定义调度器

struct MyPolicies {
    using Mixins = eventpp::MixinList<eventpp::MixinFilter>;
};
eventpp::EventDispatcher<int, void (), MyPolicies> dispatcher;

若有多个 mixin ,像是 using Mixins = eventpp::MixinList<MixinA, MixinB, MixinC> ,则继承层次如下:

EventDispatcher <- MixinA <- MixinB <- MixinC <- EventDispatcherBase

最前面的排在继承层次的最底部

可选拦截点( Optional interceptor points )

一个 mixin 可以有一些具有特殊名称的函数,这些函数会被在特定的时候调用。这些特殊的函数必须是 public 的。当前只有一个特殊函数,如下:

template <typename ...Args>
bool mixinBeforeDispatch(Args && ...args) const;

mixinBeforeDispatch 会在 EventDispatcher、EventQueue 开始调度事件之前被调用,其接收所有传给 EventDispatcher::dispatch 的参数,除了所有的参数都是以左值引用的方式传递的,无论它们在回调函数原型中是否被引用(无法修改 const 引用)。所以该函数能够修改实参,让后续的监听器都看到修改后的值。
因此该函数可以修改实参,所有的监听器看到的参数都是修改后的值。
该函数返回 true 时将继续调度,返回 false 时会停止继续调度。
对于多 mixin 的情况,该函数会被按照这些 mixin 出现在策略类的 MixinList 中的顺序来执行。

MixinFilter

MixinFilter 可以在调度开始前过滤或修改所有的事件。

MixinFilter::appendFilter(filter) 向调度器中添加新的事件过滤器。filter 接收参数的类型是带有左值引用类型的回调函数原型。
所有的事件都会在执行任何监听器前触发事件过滤器的执行。
因为所有的实参都是以左值引用的形式传递的,因此事件过滤器也能修改这些实参,无论这些实参是否被在回调原型中引用(当然,const 引用是改不了的)。

下面的表格展示了事件过滤器如何接收实参

回调原型的实参类型过滤器收到的实参类型过滤器是否能修改实参?备注
int, const intint &, int &抛弃 const 值的不变性
int &, std::string &int &, std::string &
const int &, const int *const int &, const int * &必须保持引用/指针的不变性

事件过滤器强大且实用,下面是一些用例示例

  1. 捕捉并阻塞所有感兴趣的事件。例如,在一个 GUI 窗口系统中,所有的窗口都能接收到鼠标的点击事件。然而,当一个窗口正在被鼠标拖拽时,只有被拖拽的窗口才应该能接收到鼠标事件,即使鼠标正在其他窗口上移动也应如此。所以当开始拖动时,窗口可以添加一个过滤器,该过滤器重定向所有发到窗口的鼠标事件,阻止其他窗口的监听器获得鼠标事件,但扔放行鼠标事件外的所有其他事件。
  2. 设置捕捉所有事件的监听器。例如,在一个电话本系统中,系统根据动作(action)来发送事件(删除添加电话号、查找电话号等)。如果想要在该系统中实现这样的一个模块:其只需要取电话号的区号,而不关心具体发生了的动作,该怎么做呢?一种方法是让该模块可以监听所有的事件(添加、删除、查找),但这种方法比较脆弱——若添加了一种新的动作事件,而忘记了为该模块定义相应的监听逻辑,就会导致出现未定义的行为。更好的方法是为该模块添加一个过滤器,并在过滤器中检查区号。

公共类型

FilterHandle:appendFilter 返回的句柄类型。过滤器句柄可用于移除过滤器。可以通过将 FilterHandle 实例转换为 bool 类型来检查其是否为空,当值为 false 时,该句柄为空。FilterHandle 是可拷贝的。

函数

FilterHandle appendFilter(const Filter & filter);

filter 添加进调度器。

该函数将返回一个可用于 removeFilter 的过滤器句柄

bool removeFilter(const FilterHandle & filterHandle);

从调度其中移除一个过滤器。

当过滤器被成功移除时返回 true。

MixinFilter 示例代码

代码

struct MyPolicies {
    using Mixins = eventpp::MixinList<eventpp::MixinFilter>;
};
eventpp::EventDispatcher<int, void (int e, int i, std::string), MyPolicies> dispatcher;

dispatcher.appendListener(3, [](const int e, const int i, const std::string & s) {
    std::cout
        << "Got event 3, i was 1 but actual is " << i
        << " s was Hello but actual is " << s
        << std::endl
    ;
});
dispatcher.appendListener(5, [](const int e, const int i, const std::string & s) {
    std::cout << "Shout not got event 5" << std::endl;
});

// 添加三个事件过滤器.

// 第一个过滤器将输入的实参改为其他值,然后后续的过滤器及监听器将看到修改后的值
dispatcher.appendFilter([](const int e, int & i, std::string & s) -> bool {
    std::cout << "Filter 1, e is " << e << " passed in i is " << i << " s is " << s << std::endl;
    i = 38;
    s = "Hi";
    std::cout << "Filter 1, changed i is " << i << " s is " << s << std::endl;
    return true;
});

// 第二个过滤器将所有 5 事件都过滤出来。因此监听事件 5 的过滤器都不会被触发。
// 第三个过滤器也不会在事件 5 触发
dispatcher.appendFilter([](const int e, int & i, std::string & s) -> bool {
    std::cout << "Filter 2, e is " << e << " passed in i is " << i << " s is " << s << std::endl;
    if(e == 5) {
        return false;
    }
    return true;
});

// 第三个过滤器只打印输入的实参
dispatcher.appendFilter([](const int e, int & i, std::string & s) -> bool {
    std::cout << "Filter 3, e is " << e << " passed in i is " << i << " s is " << s << std::endl;
    return true;
});

// 调度所有事件,第一个实参总是事件类型
dispatcher.dispatch(3, 1, "Hello");
dispatcher.dispatch(5, 2, "World");

输出

Filter 1, e is 3 passed in i is 1 s is Hello
Filter 1, changed i is 38 s is Hi
Filter 2, e is 3 passed in i is 38 s is Hi
Filter 3, e is 3 passed in i is 38 s is Hi
Got event 3, i was 1 but actual is 38 s was Hello but actual is Hi
Filter 1, e is 5 passed in i is 2 s is World
Filter 1, changed i is 38 s is Hi
Filter 2, e is 5 passed in i is 38 s is Hi