C++事件委托——在Poco C++ Libraries中的实现(闲着无聊,解析下源码)

先做一段翻译吧,毕竟这个库目前用的人还不多,但是的确是一个优秀的跨平台C++ libraries。

Poco C++库是由一系列开源的C++类库组成,可以非常简单高效地开发出可移植的跨平台C++程序(包含跨平台的网络类库等)。可以在这里获得:https://pocoproject.org/index.html

Poco C++库不仅完全兼容C++标准库,更弥补了很多标准库的不足(C++11同样也补充了很多新标准,但是目前市场上没有一个编译器能够完全支持)。个人认为相比于其它C++程序库(例如boost C++),Poco可以说是非常短小精悍。而且高度的模块化和高效的设计与实现让Poco非常适合嵌入式应用。Poco充分利用了C++的特性:面向底层的掌控(设备 I/O等等)和高层的面向对象开发。这也是优秀的C++程序库应该具备的,这也是为什么C++始终在编程语言中如此强大的原因了。C++在底层操控和高层逻辑处理上扮演了很关键的中间角色,一旦能将各平台相关的API利用C++特性抽象成高级的统一的面向对象描述,那么这个C++库才是一个优秀的”轮子”,这样作为库的使用者(即应用开发者)就能够集中关注于应用自身。

废话了这么多,虽然不用重新造”轮子”,但是了解”轮子”构造才是本文的主题。

先来看看Poco C++库和Boost C++库的直观对比:

Boost C++ Libraries

011713_0908_CPoco1.png

Poco C++ Libraries

011713_0908_CPoco2.png

很明显,Boost C++中充斥着格式各样的宏定义,或许这在C函数库里很常见,但是我觉得可以不这么复杂,给新手学习成本很大。反观Poco C++库,没有臃肿的宏定义,没有长串的名称空间,大部分函数都返回void和bool,函数体都不大。

嗯,又废话了些,直入主题,C++事件委托。Poco官方给出的示例是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "Poco/BasicEvent.h"

#include "Poco/Delegate.h"

#include <iostream>

using Poco::BasicEvent;

using Poco::Delegate;
class Source
{
public:
BasicEvent<int> theEvent;

void fireEvent(int n)
{
theEvent(this, n);
}
};

class Target
{
public:
void onEvent(const void* pSender, int& arg)
{
std::cout << "onEvent: " << arg << std::endl;
}
};

int main(int argc, char** argv)
{
Source source;
Target target;

source.theEvent += Delegate<Target, int>(
&target, &Target::onEvent);

source.fireEvent(42);

source.theEvent -= Delegate<Target, int>(
&target, &Target::onEvent);

return 0;
}

嗯,BasicEvent提供一个operator+= 和一个operator-=实现事件的注册和注销,以及事件响应的operator()。注册事件接受的参数是一个委托类(delegate),可以支持注册多个事件响应,因此我们可以设想在其内部用一个容器来存放这些注册的回调函数信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template <class TArgs,class TStrategy>
// 以事件响应函数参数和响应策略代理类为模板参数,后者稍后介绍
class BasicEvent:
{
public:
BasicEvent(){}

~BasicEvent(){}
///////////////////////////////////////////////
// 三个主要的成员函数接口
void operator += (const Delegate& aDelegate)
/// Adds a delegate to the event.
{
_strategy.add(aDelegate);
}

void operator -= (const Delegate& aDelegate)
/// Removes a delegate from the event.
///
/// If the delegate is not found, this function does nothing.
{
_strategy.remove(aDelegate);
}

void operator () (const void* pSender, TArgs& args)
/// Shortcut for notify(pSender, args);
{
// 用策略代理(Proxy)类响应所有注册函数,同样稍后解释
_strategy.notify(pSender, args);
}
///////////////////////////////////////////////

protected:
TStrategy _strategy;
private:
// 禁用拷贝构造和复制拷贝
BasicEvent(const BasicEvent& e);
BasicEvent& operator = (const BasicEvent& e);
};

通常管理多个事件会遇到这样的实例:例如,对于普通事件响应采用FIFO方法是很好容易理解的常规策略,但是对于I/O设备事件响应的问题更多时候应该采用优先级策略,而这种优先级的处理也是Poco Event库应该所具有的功能。在这种特例问题上,常常会犯地一个错误就是认为可以很直观地从BasicEvent类派生一个PriorityEvent子类。基类和子类的关系应该永远是is-a的联系。这里并不存在PriorityEvent隶属于BasicEvent,相反两种事件类型从库的使用者(即应用开发者)角度来说有着一致的行为:注册、响应和注销事件。

唯一不同在于PriorityEvent比BasicEvent多一个优先级参数。因此,一致的行为可以抽象为两者的一个基类(AbstractEvent,这个基类实现了事件的统一接口,但是不是虚基类),而不同的处理策略才是二者的参数。事实上不仅仅对应用开发者,而且对AbstractEvent来说,处理事件的接口行为也是只有三个:注册、响应和注销事件。因此可以把执行响应的策略抽象为一个代理类来执行。现代C++中为一个抽象基类实现不同行为特性的常用方法是类模板,准确点说是模板的特化与偏特化。这是很多C++程序库中经常用的技巧。

下面主要给出AbstractEvent的部分源码,BasicEvent和PriorityEvent就简单很多了。至于委托类(Delegate)简单来说就是包含了回调的委托函数指针及其拥有者的对象指针的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
template <class TArgs, class TStrategy, class TDelegate, class TMutex = FastMutex>
class AbstractEvent
{
public:
typedef TArgs Args;

AbstractEvent():
_executeAsync(this, &AbstractEvent::executeAsyncImpl)
{
}

virtual ~AbstractEvent()
{
}

void operator += (const TDelegate& aDelegate)
/// Adds a delegate to the event.
/// Exact behavior is determined by the TStrategy.
{
// 作用域内的互斥对象,析构时释放互斥锁
typename TMutex::ScopedLock lock(_mutex);
// 策略类中包含了用于存放委托事件的容器
_strategy.add(aDelegate);
}

void operator -= (const TDelegate& aDelegate)
/// Removes a delegate from the event.
/// If the delegate is not found, this function does nothing.
{
typename TMutex::ScopedLock lock(_mutex);
_strategy.remove(aDelegate);
}

void operator () (const void* pSender, TArgs& args)
/// Shortcut for notify(pSender, args);
{
notify(pSender, args); //执行所有响应
}

void operator () (TArgs& args)
/// Shortcut for notify(args).
{
notify(0, args);
}
void notify(const void* pSender, TArgs& args)
{
Poco::ScopedLockWithUnlock<TMutex> lock(_mutex);

// 为了线程安全(thread-safeness):
// 先拷贝到局部变量,防止响应委托事件时容器变更
TStrategy strategy(_strategy);
lock.unlock();
strategy.notify(pSender, args);
}

void clear()
/// Removes all delegates.
{
typename TMutex::ScopedLock lock(_mutex);
_strategy.clear();
}

bool empty() const
/// Checks if any delegates are registered at the delegate.
{
typename TMutex::ScopedLock lock(_mutex);
return _strategy.empty();
}

protected:
TStrategy _strategy; /// The strategy used to notify observers.
mutable TMutex _mutex;

private:
AbstractEvent(const AbstractEvent& other);
AbstractEvent& operator = (const AbstractEvent& other);
};

上面给出了主要的实现代码,对于BasicEvent和PriorityEvent来说,只需要继承自AbstractEvent特化出的模板类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template <class TArgs, class TMutex = FastMutex>
class BasicEvent: public AbstractEvent <
TArgs, DefaultStrategy<TArgs, AbstractDelegate<TArgs> >,
AbstractDelegate<TArgs>,
TMutex
>
/// A BasicEvent uses the DefaultStrategy which
/// invokes delegates in the order they have been registered.
/// DefaultStrategy采用FIFO策略处理事件响应
{
public:
BasicEvent(){}

~BasicEvent(){}

private:
BasicEvent(const BasicEvent& e);
BasicEvent& operator = (const BasicEvent& e);
};

template <class TArgs, class TMutex = FastMutex>
class PriorityEvent: public AbstractEvent <
TArgs,
PriorityStrategy<TArgs, AbstractPriorityDelegate<TArgs> >,
AbstractPriorityDelegate<TArgs>,
TMutex
>
{// 和BasicEvent相同
};

从PriorityEvent可以看出,PriorityEvent只是针对优先级策略采用了PriorityStrategy 和AbstractPriorityDelegate,后者相对于普通的AbstractDelegate多了一个优先级参数而已。在Poco C++的自身库内约定,BasicEvent固定搭配DefaultStrategy, PriorityEvent固定搭配PriorityEvent,而在使用时需要注意operator+=和operator-=,必须提供与之对应的Delegate类型,任何优秀的C++库都有自身的编程约定,作为库的使用者,只有遵循这些规则才能玩好这个游戏。

1
2
3
4
5
6
7
8
BasicEvent<int> theBasicEvent;
// 不会报错,但事实上会采用FIFO策略
theBasicEvent += PriorityDelegate<Target, int>( // Logic Error,no compile error
&target, &Target::onEvent,0);// 优先级0

PriorityEvent<int> thePriorityEvent;
thePriorityEvent += Delegate<Target, int>( // Compile error,没有优先级属性。
&target, &Target::onEvent);// 没有优先级参数

还有两点可能大家已经注意到:

  1. 只支持单个参数的回调函数注册;
  2. 不支持回调函数返回值检测。

对于1来说,Poco C++文档给出的方法是传递一个包含多个参数的结构体来解决。
对于2来说,可以通过响应函数提供non-const 的sender引用来修改当前被调者当前的状态,Arg的引用也是可以被接受的: 

1
void onEvent(const void* pSender, int& arg);

这里又回到了开始的话题,look,回调函数的返回值永远是void,既有自我约束,又有灵活巧妙,既简单又富含编程哲学,这就是Poco C++ Libraries。