原文地址:细说C++委托和消息反馈模板
C++实现委托和消息反馈模板:
继承+多态
乍一看是理所当然的选择,库中的类把响应处理函数设置为虚函数,客户程序可以继承这个类并且重载响应函数。以某个Socket类为例,可以提供一个OnRecv函数用来响应网络数据包到达的处理。客户程序只需要重载OnRecv并进行自己的处理就可以了。
- struct Socket { // base class
- virtual void OnRecv();
- };
- stuct MySocket { // your event-handle class
- virtual void OnRecv() { /* do sth here ... */ }
- }
疑问:很多时候这样做实在很烦,特别是做小程序的时候,或者需要快速做原型的时候,一眼望去小小的程序一上来就继承了一大堆东西,颇为不爽。只是想着能省事一点,希望能像那些脚本语言一样快速绑定消息响应,而不是以继承开始工作——我已经害怕看到长长的类继承树了,很多时候根本不必要继承整个类;又或者某些类只提供一个接口而不是具体的类又或者需要多重继承,处理都有一定麻烦;最麻烦的莫过于有时候需要改变响应处理,难道继承好几个下来么——这么多虚表也是浪费啊。
点评:为了使用Socket就必须继承Socket,这可以说是Socket的设计的问题。如果需要实现类似的功能的话,可以写成如下,虽然和继承 Socket 没有多少本质的差别,不过确实把消息处理类和Socket的实现扯开了。
- struct SocketEventHandler {
- virtual void OnRecv() { /* ... */ }
- virtual void OnSend() { /* ... */ }
- };
- struct Socket {
- void set_handler( SocketEventHandler* h ) { handler_ = h; }
- private:
- SocketEventHandler* handler_;
- };
- struct MyHandler : SocketEventHandler {
- void OnRecv() { ... }
- };
- Socket s;
- MyHandler h;
- s.set_handler( &h );
丢开继承,我们有没有一种简单明确的表达方法呢?我不禁想起了c时代的回调函数……
回调函数(CallBack)
非常简单,就是一个函数指针。刚才的OnRecv可以写成这样
- struct Socket {
- void OnRecv() { if(OnRecvHandle!=NULL) OnRecvHandle(); }
- void (*OnRecvHandle) ();
- };
客户程序只需要编写一个MyOnRecv函数,并且赋值给OnRecvHandle就可以了
- void MyOnRecv(); // your event-handle function
- Socket foo;
- foo.OnRecvHandle = MyOnRecv;
疑问:非常简单,不需要继承类就可以处理,而且随时可以替换不同的处理函数。其实多态的本质也是函数指针,只不过多态是用vtable统一管理函数指针。回调函数要特别注意函数指针是否为空的问题,因此最好外面在包装一层判断过程,回调函数最大问题在于类型不安全。
委托(Delegation)
委托是什么呢?委托最本质的是提供一种类型安全的动态消息响应转移机制。
以前,我对委托一无所知,我觉得无非就是一个类型安全的智能指针,而所谓的Multi-Cast Delegation无非就是一个智能指针数祖,是不是还有Any-Cast Delegation呢?我不知道,也许有吧,无非就是智能指针数祖+随机数发生器。
但是,实际上并不是那么简单。你可以把我刚才说的函数指针封装一下弄一个类封装起来,不过,这直接导致某个消息的响应只能是固定死的函数指针类型,甚至不能是可爱的Functor或者是某个类的成员函数。你可能会跟我抬杠说这怎么可能,不是可以用template实现么?我们来看一个例子
假设某个委托类 Dummy_Delegation 拥有一个成员函数用来连接处理函数 template <class T> void Dummy_Delegation::Connect(T _F); 没错,_F可以不一定函数指针,也可以是Functor,我们利用_F()来呼叫响应函数,一切看起来是多么美好——但是,很不幸,这个_F无法保存下来供消息产生的时候呼叫。
一切都因为这个该死的template<class T> ,你无法在Dummy_Delegation内定义一个T类型的变量或者指针来保存_F。退一万步说,你把T作为整个Dummy的模版,还是避免不了在模版实例化的时候定死类型。于是,整个Delegation的通用性大打折扣。
实际上,我们希望有这么一种Delegation,他可以把消息响应动态绑定到任何一个类的成员函数上只要函数类型一致。注意,这里说的是任何一个类。这就要求我们屏蔽信号发生器和响应类之间的耦合关系,即,让他们相互都不知道对方是谁甚至不知道对方的类型信息。
这个方法可行么?Yes!
桥式委托(Bridge Delegation) ---- 利用泛型+多态来实现
请允许我杜撰一个名词:桥式委托(Bridge Delegation)
实现这么一个东西真的很有意思,其实,像gtk+/qt很多需要"信号/反馈"(signal/slot)的系统都是这么实现的。
说到GP和Template,那真的可以算是百家争鸣了,就像boost和loki还在争夺新的C++标准智能指针的地位打得不可开交。而Functor这个东西有是很多GP algo的基础,比如sort/for_each等等。
整个桥式委托的结构如下图:
- Signal <>-------->* Interface
- ^
- |
- Implementation<Receiver> -------------> Receiver
我们搭建了一个Interface/Implementation的桥用来连接Singal和Receiver,这样就可以有效隔开双方的直接耦合。用之前我们的Socket类来演示如下:
- struct Socket {
- Signal OnRecv;
- };
一个Receiver可以是一个function比如 void OnRecv1() 也可以是一个Functor:
- struct OnRecv2_t {
- void operator() ();
- } OnRecv2;
我们可以这样使用这个桥式委托
- Socket x;
- x.OnRecv.ConnectSlot(OnRecv1); //或者 x.OnRecv.ConnectSlot(OnRecv2());
当消息产生调用 x.OnRecv()的时候,用户指定的OnRecv1或者OnRecv2就会响应。
我们来看看如何实现这个桥:首先是一个抽象类
- struct DelegationInterface {
- virtual ~DelegationInterface() {};
- virtual void Action() = 0;
- };
然后才是模版类Impl:
- template<class T>
- struct DelegationImpl : public DelegationInterface {
- T _FO;
- DelegationImpl(T _S) :_FO(_S) { }
- virtual void Action() { _FO(); }
- };
注意我们上面的图示,这个DelegationImpl类是跟Receiver相关联的,也就是说这个Impl类知道所有的Receiver细节,于是他可以从容地调用Receiver()。再次留意这个继承关系,对了,一个virutal的Action函数!利用多态性质,我们可以根据Receiver来实例化DelegationImpl类,却可以利用提供一致的访问Action的Interface,这就是整座桥的秘密所在——利用多态下层隔离细节!
再看看我们的Signal类:
- struct Signal {
- DelegationInterface* _PI;
- Signal() :_PI(NULL) {}
- ~Signal() { delete _PI; }
- void operator()() { if(_PI) _PI->Action(); }
- template<class T> void ConnectSlot(T Slot) {
- delete _PI; _PI = new DelegationImpl<T>(Slot);
- }
- };
显然,Signal类利用了 DelegationInterface* 指针_PI来呼叫响应函数。而完成这一切连接操作的正是这个奇妙的ConnectSlot的函数。对了!上次讨论模版函数的时候就说了这个T类型无法保存,但是这里用桥避开了这个问题。利用模版函数的T做为DelegationImpl的实例化参数,一切就这么简单地解决了。
你也许可能会抗议,认为我绕了一大圈又绕回了一开始我烦恼的继承/多态上面来了。其实,你有没有发现,我们这个Singal/Bridge Delegation/Receive的体系是固定的一套东西,你在实际使用中并不需要自己去继承去处理重载,你只需要好好地Connect到正确的Slot就可以了。这也可以算是一种局部隐含的继承吧。
接下来我们要讨论一下这个桥式委托的性能消耗以及扩展和局限性问题
桥式委托的进一步研究
看过上面的桥式委托之后,可能会有点怀疑他的性能,需要一个interface指针一个functor类/函数指针,调用的时候需要一次查vtable,然后再一次做operator()调用。其实,这些消耗都不算很大的,整个桥式委托的类结构是简单的,相对于前面说的继承整个类之类的做法开销还是比较小的,而且又比函数指针通用而且类型安全。最重要的是,刚才的Signal可以方便地改写为Multi-Cast Delegation即一个信号引发多个响应——把Singal内部的DelegationInterface*指针改为一个指针队列就可以了。
不过,我们刚才实现的桥式委托只能接收函数指针和functor,不能接收另外一个类的成员函数,有时候这是非常有用的动作。比如设置一个按钮Button的OnClick事件的响应为一个MsgBox的Show方法。当然,MsgBox还有其他非常多的方法,这样就可以不用局限于把MsgBox当成一个functor了。
我们要改写刚才的整个桥来实现这个功能,在这里需要你对指向成员函数得指针有所了解。
- // 新版的桥式委托,可以接收类的成员函数作为响应
- struct DelegationInterface {
- virtual ~DelegationInterface() {};
- virtual void Run() = 0;
- };
- template<class T>
- struct DelegationImpl : public DelegationInterface {
- typedef void (T::* _pF_t)(); // 指向类T成员函数的指针类型
- DelegationImpl(T* _PP, _pF_t pF) :_P(_PP), _PF(pF) {}
- virtual void Run() {
- if(_P) { (_P->*_PF)(); } // 成员函数调用,很别扭的写法(_P->*_PF)();
- }
- T* _P; // Receiver类
- _pF_t _PF; // 指向Receiver类的某个成员函数
- };
- struct Signal
- {
- DelegationInterface* _PI;
- Signal() :_PI(NULL) {}
- void operator() () { if(_PI) _PI->Run(); }
- // 新的ConnectSlot需要指定一个类以及这个类的某个成员函数
- template<class T>
- void ConnectSlot(T& recv, void (T::* pF)()) { // pF这个参数真够别扭的
- _PI = new DelegationImpl<T>(&recv, pF);
- }
- };
注意:ConnectSlot方法的pF参数类型非常复杂,也可以简化如下,即把这个类型检测推到DelegationImpl类去完成,而不在Connect这里进行么?编译器可以正确识别。对于模板来说,很多复杂的参数类型都可以用一个简单的类型代替,不用关心细节,就象上面用一个F代替void (T::*)()。有时候能改善可读性,有时候象反。
- template<class T, class F>
- void ConnectSlot( T& recv, F pF ) {
- PI_ = new DelegationImpl<T>(&recv,pF);
- }
这个新版怎么用呢,很简单的。比如你的MsgBox类有一个成员函数Show,你可以把这个作为响应函数:
- MsgBox box;
- Socket x; // Socket还跟旧的版本一样
- x.OnRecv.ConnectSlot(box, &MsgBox::Show);
注意上面这里引用成员函数指针的写法,一定不能写成box.Show,呵呵,希望你还记得成员函数是属于类公共的东西,不是某个实例的私有产品。大家不妨进一步动一下脑筋,把新版的Signal和旧版的Signal结合一下,你就可以获得一个功能超强的Delegation系统了。
点评:用signal的办法确实可以方便地动态替换处理函数,不过这是以每个可能被处理的消息都要在每个对象中占用一个 signal 的空间为代价的。而且,需要动态改变处理函数的应用我已经不记得什么时候见过了。即使有,也可以通过在override的virtual函数里自己处理实现,虽说麻烦,但也是可能的。此外,以上代码并不够规范,下划线加大写字母开头的标识符是保留给语言的实现用的。
结论
我们关于桥式委托的讨论接近尾声了,大家也许已经发现了一个巨大的问题:上面的桥式委托无法给相应操作传递参数!!!是的,这是一个巨大的矛盾——你必须自己实现带一个参数的桥、自己实现带2个参数的桥……就像stl的functor一样,你无法做到参数通用处理,必须区分unary_functor、binary_functor……你不得不这么做:
- template<class P1>
- struct DelegationInterface { virtual void Run(P1 param) = 0; };
- template<class T, class P1>
- struct DelegationImpl : public DelegationInterface<P1> {
- ......
- }
- template<class P1>
- struct Signal {
- DelegationInterface<P1> *_PI;
- ......
- }
以上、你会发现自己写了太多桥了,当然了,你可以绕路来实现,比如用一个通用的打包参数来包装多个参数,用宏定义来处理各种情况,当然也可以用预处理来实现——我这里要说的,同情一下QT吧,不要整天抱怨他的signal/slot体系需要预处理是在扩展语言——设身处地地想一想,C++提供给我们的就这有这些了,一个小小的参数是我们这些signal/slot抹不去的伤痛。
幸运的是,在C++标准委员会不断的努力之下,这些情况开始有所改善。boost库之中的signal库可以直接支持可变参数的委托;同时,越来越多的元语言技术也引入了C++之中。虽然目前支持这些新特性的编译器还比较少,不过这已经是非常巨大的进步了,让我们期待吧!
原文:[C++]实现委托模型
成员函数指针的操作
在开始之前首先介绍一下成员函数指针,它与非成员函数指针的操作方式有很大的不同。有这么一个类:
1
2
3
4
|
class
A {
public
:
void
Func(
int
) { … }
};
|
要取得Func函数的指针,必须这么做:
1
|
void
(A::*pFunc)(
int
) = &A::Func;
|
::*是一个特殊的操作符,表示pFunc是一个指针,指向A的成员函数。获取成员函数的地址不能通过类对象来获取,必须像上面的那样,通过类名获取,而且要加上取地址操作符(&)。
那么如何通过成员函数指针来调用该函数呢?成员函数都有一个隐含的this参数,表示函数要操作的对象,现在我们只获取到了函数的指针,还缺少一个对象作为this参数。为了达到这个目的,需要先创建一个对象,然后通过该对象来调用成员函数指针:
1
2
3
4
5
|
A a;
(a.*pFunc)(10);
A* pa = &a;
(pa->*pFunc)(11);
|
第一种方式是通过对象本身来调用,第二种方式是通过对象指针来调用,两种方式的效果都是一样的。.*和->*都是特殊的操作符,不必纠结于它们奇怪的样子,只要知道它们只用于调用成员函数指针就行了。
第一步:使用类模板
通过上面的介绍,我们知道了要调用一个成员函数,仅仅有成员函数指针是不够的,还需要一个对象指针,所以要用一个类将两者绑到一起。由于对象的类型是无穷多的,所以这里必须使用类模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
template
<
typename
T>
class
DelegateHandler {
public
:
DelegateHandler(T* pT,
void
(T::*pFunc)(
int
))
: m_pT(pT), m_pFunc(pFunc) { }
void
Invoke(
int
value) {
(m_pT->*m_pFunc)(value);
}
private
:
T* m_pT;
void
(T::*m_pFunc)(
int
);
};
|
可以像下面那样使用该模板:
1
2
3
4
5
6
7
|
A a;
DelegateHandler<A> ah(&a, &A::Func);
ah.Invoke(3);
B b;
DelegateHandler<B> bh(&b, &B::Method);
//B::Method的声明与A::Func一致
bh.Invoke(4);
|
到这里产生了一个问题:如果希望调用的目标是非成员函数,怎么办?上面的类模板无法调用非成员函数,不过使用模板偏特化就可以解决这个问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template
<>
class
DelegateHandler<
void
> {
public
:
DelegateHandler(
void
(*pFunc)(
int
))
: m_pFunc(pFunc) { }
void
Invoke(
int
value) {
(*m_pFunc)(value);
}
private
:
void
(*m_pFunc)(
int
);
};
|
使用方法也是一样的:
1
2
|
DelegateHandler<
void
> h(NonmemberFunc);
// void NonmemberFunc(int);
h.Invoke(5);
|
也许你会有疑问:非成员函数不需要将函数指针和对象指针绑到一起,为什么这里还要用一个类来包装函数指针?看了下面的内容自然会明白了。
第二步:使用多态
对于单目标的委托来说,使用上面的代码或许就已经足够了。但是我的目的当然不止于此,我想要的是多目标的委托。多目标委托其实就是一个容器,在这个容器里可以存放多个对象,当调用委托的时候依次调用每个对象。容器里的对象应该都是相同的类型,这样才能够放到强类型的容器中;而且委托调用方不应该知道具体的调用目标是什么,所以这些对象也应该要隐藏具体的细节。遗憾的是,上一步中实现的类模板都不具备这些能力,DelegateHandler<A>和DelegateHandler<B>是不同的类型,不能放到同一个容器中,调用方要调用它们也必须知道调用的目标是什么类型。
解决这个问题的方法就是使用多态,令所有的委托目标类都继承一个公共的接口,调用方只通过这个接口来进行调用,这样就不必知道每个目标具体的类型。下面就是该接口的定义:
1
2
3
4
5
6
|
class
IDelegateHandler {
public
:
virtual
~IDelegateHandler() { }
virtual
void
Invoke(
int
) = 0;
};
|
然后令DelegateHandler继承该接口:
1
2
3
4
5
6
7
8
9
|
template
<
typename
T>
class
DelegateHandler :
public
IDelegateHandler {
…
}
template
<>
class
DelegateHandler<
void
> :
public
IdelegateHandler {
…
}
|
现在可以将各种类型的DelegateHandler放到同一个容器中,并使用同样的方式来调用了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
A a;
B b;
DelegateHandler<A> ah(&a, &A::Func);
DelegateHandler<B> bh(&b, &B::Method);
DelegateHandler<
void
> vh(NonmemberFunc);
std::vector<IDelegateHandler*> handlers;
handlers.push_back(&ah);
handlers.push_back(&bh);
handlers.push_back(&vh);
for
(
auto
it = handlers.cbegin(); it != handlers.cend(); ++it) {
(*it)->Invoke(7);
}
|
第三步:使用宏
不知道你注意到没有,上面写了那么多代码,只是为了实现一个返回值为void,有一个int参数的委托!如果要实现更多类型的委托,上面的代码就要重复很多次了。幸好,C++有宏这个东西,使用它可以帮助我们快速生成大量代码。然而这个宏的定义可不是那么简单,为了它我费了好大周折。下面开始讲述这个探索的过程,如果不想看我啰嗦,可以直接跳到后面看现成的代码。
我们都知道,函数参数的声明可以只有类型而没有名称,但是为了在函数内使用参数,该参数必须有名称。例如:
1
2
3
4
5
6
|
void
Invoke(
int
) {
//不能使用参数
}
void
Invoke(
int
value) {
//可以通过value这个名称来使用参数
}
|
另外,调用函数的时候只能使用名称,不能带有类型:
1
2
|
int
value = 10;
Invoke(value);
|
这些问题似乎都显而易见,根本不值一提,但这些就是定义宏的关键。一开始我想象宏的使用应该是这样的:
1
|
DELEGATE(
void
, DelegateHandler,
int
,
int
);
|
毫无疑问,在它的定义中,从第三个参数开始应该使用可变参数,像这样(只截取了定义的一部分):
1
2
3
4
5
6
|
#define DELEGATE(retType, name, …) \
…
retType Invoke(__VA_ARGS__) { \
return
(*m_pFunc)(__VA_ARGS__); \
} \
…
|
展开后的代码是这样的:
1
2
3
4
5
|
…
void
Invoke(
int
,
int
) {
return
(*m_pFunc)(
int
,
int
);
}
…
|
这样很明显是错误的,即使在定义委托的时候加上参数名称也不行。问题的原因是函数参数的声明方式与调用方式不同,而且我们不能将__VA_ARGS__拆开来处理,我们没办法为参数添加名称,也不能去掉参数的名称。
既然如此,我们就使用两个__VA_ARGS__,一个用于函数参数的声明,一个用于调用。以上面的为例,第一个__VA_ARGS__应该是这样子:
1
|
int
a,
int
b
|
第二个__VA_ARGS__应该是这样子:
1
|
a, b
|
宏展开之后应该是这样子:
1
2
3
4
5
|
…
void
Invoke(
int
a,
int
b) {
return
(*m_pFunc)(a, b);
}
…
|
这样就正确了。可是这样又带来了一个新问题:一个宏里只能使用一个可变参数。解决方法是,使用另外的宏来产生这两个__VA_ARGS__!好了,我不再说废话了,直接给出代码来,代码比我的表达能力更强。
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
#define DECLARE_PARAMS(...) __VA_ARGS__
#define DECLARE_ARGS(...) __VA_ARGS__
//0个参数的委托
#define DELEGATE0(retType, name) \
DECLARE_DELEGATE(retType, name, DECLARE_PARAMS(
void
), )
//1个参数的委托
#define DELEGATE1(retType, name, p1) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a), \
DECLARE_ARGS(a))
//2个参数的委托
#define DELEGATE2(retType, name, p1, p2) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b), \
DECLARE_ARGS(a, b))
//3个参数的委托
#define DELEGATE3(retType, name, p1, p2, p3) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b, p3 c), \
DECLARE_ARGS(a, b, c))
//4个参数的委托
#define DELEGATE4(retType, name, p1, p2, p3, p4) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b, p3 c, p4 d), \
DECLARE_ARGS(a, b, c, d))
//5个参数的委托
#define DELEGATE5(retType, name, p1, p2, p3, p4, p5) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b, p3 c, p4 d, p5 e), \
DECLARE_ARGS(a, b, c, d, e))
//6个参数的委托
#define DELEGATE6(retType, name, p1, p2, p3, p4, p5, p6) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b, p3 c, p4 d, p5 e, p6 f), \
DECLARE_ARGS(a, b, c, d, e, f))
//7个参数的委托
#define DELEGATE7(retType, name, p1, p2, p3, p4, p5, p6, p7) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b, p3 c, p4 d, p5 e, p6 f, p7 g), \
DECLARE_ARGS(a, b, c, d, e, f, g))
//8个参数的委托
#define DELEGATE8(retType, name, p1, p2, p3, p4, p5, p6, p7, p8) \
DECLARE_DELEGATE( \
retType, \
name, \
DECLARE_PARAMS(p1 a, p2 b, p3 c, p4 d, p5 e, p6 f, p7 g, p8 h), \
DECLARE_ARGS(a, b, c, d, e, f, g, h))
#define DECLARE_DELEGATE(retType, name, params, args) \
class
I##name { \
public
: \
virtual
~I##name() { } \
virtual
retType Invoke(params) = 0; \
}; \
template
<
typename
T> \
class
name :
public
I##name { \
public
: \
name(T* pType, retType (T::*pFunc)(params)) \
: m_pType(pType), m_pFunc(pFunc) { } \
retType Invoke(params) { \
return
(m_pType->*m_pFunc)(args); \
} \
private
: \
T* m_pType; retType (T::*m_pFunc)(params); \
}; \
template
<> \
class
name<
void
> :
public
I##name { \
public
: \
name(retType (*pFunc)(params)) \
: m_pFunc(pFunc) { } \
retType Invoke(params) { \
return
(*m_pFunc)(args); \
} \
private
: \
retType (*m_pFunc)(params); \
}
|
注意最后面少了一个分号,这是故意为之的,为了强迫在定义委托的时候加上分号。这种宏定义的方法对参数个数有了限制,我这里的定义最多只支持8个参数,为了支持更多参数,需要写更多的代码。其实我认为8个参数已经足够了,超过8个参数的函数不是好的设计,应该重新考虑一下。