委托、信号和动静反馈的模板实现技能
副标题#e#
憋了很长一段时间的想法,在这里说说,但愿听听诸位好手的意见。
我写过不少C++措施(虽然比起好手照旧差远了),写过库也写过客户措施。一般库城市提供一些好用的类供客户措施利用,不少库还可以让客户措施响应库内的某些事件。好比MFC/ATL/VCL提供动静响应,许多ActiveX提供自界说动静响应,甚至是系统底层的间断挪用都可以列入这个领域。然而,正是以上这些“反向”的挪用让我以为很烦恼。
1 担任+多态
乍一看是理所虽然的选择,库中的类把响应处理惩罚函数配置为虚函数,客户措施可以担任这个类而且重载响应函数。以某个Socket类为例,可以提供一个OnRecv函数用来响应网络数据包达到的处理惩罚。客户措施只需要重载OnRecv并举办本身的处理惩罚就可以了。
struct Socket { // base class
virtual void OnRecv();
};
stuct MySocket { // your event-handle class
virtual void OnRecv() { /* do sth here ... */ }
}
疑问:许多时候这样做实在很烦,出格是做小措施的时候,可能需要快速做原型的时候,一眼望去小小的措施一上来就担任了一大堆对象,颇为不爽。只是想着能省事一点,但愿能像那些剧本语言一样快速绑定动静响应,而不是以担任开始事情——我已经畏惧看到长长的类担任树了,许多时候基础不须要担任整个类;又可能某些类只提供一个接口而不是详细的类又可能需要多重担任,处理惩罚都有必然贫苦;最贫苦的莫过于有时候需要改变响应处理惩罚,莫非担任好几个下来么——这么多虚表也是挥霍啊。
wangtianxing老大点评:为了利用Socket就必需担任Socket,这可以说是Socket的设计的问题。假如需要实现雷同的成果的话,可以写成如下,固然和担任 Socket 没有几多本质的不同,不外确实把动静处理惩罚类和Socket的实现扯开了。:
#p#副标题#e#
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时代的回调函数……
2 回调函数(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统一打点函数指针。回调函数要出格留意函数指针是否为空的问题,因此最好外面在包装一层判定进程。回调函数最大问题在于范例不安详,显式指针这对象……不说也罢……翻了一下智能指针和模版,我发明白一根稻草……
3 委托(Delegation)
委托是什么冬冬?这个名词好像是时尚的代名词,我似乎看到学java/c#的兄弟们在讥笑我们的落伍……其实,property不也可以算是一种委托吗?说白了不就是智能指针么?
我以为委托最本质的是提供一种范例安详的动态动静响应转移机制。
以前,我对委托一无所知,我以为无非就是一个范例安详的智能指针,而所谓的Multi-Cast Delegation无非就是一个智能指针数祖……是不是尚有Any-Cast Delegation呢?我不知道,也许有吧,无非就是智能指针数祖+随机数产生器……
可是,实际上并不是那么简朴。你可以把我适才说的函数指针封装一下弄一个类封装起来,不外,这直接导致某个动静的响应只能是牢靠死的函数指针范例,甚至不能是可爱的Functor可能是某个类的成员函数。你大概会跟我抬杠说这怎么大概,不是可以用template实现么?我们来看一个例子
假设某个委托类 Dummy_Delegation 拥有一个成员函数用来毗连处理惩罚函数 template<class T> void Dummy_Delegation::Connect(T _F); 没错,_F可以不必然函数指针,也可以是Functor,我们操作_F()来呼唤响应函数,一切看起来是何等优美——可是,很不幸,这个_F无法生存下来供动静发生的时候呼唤……
#p#分页标题#e#
一切都因为这个活该的template<class T>,你无法在Dummy_Delegation内界说一个T范例的变量可能指针来生存_F。退一万步说,你把T作为整个Dummy的模版,照旧制止不了在模版实例化的时候定死范例。于是,整个Delegation的通用性大打折扣……
实际上,我们但愿有这么一种Delegation,他可以把动静响应动态绑定到任何一个类的成员函数上只要函数范例一致。留意,这里说的是任何一个类。这就要求我们屏蔽信号产生器和响应类之间的耦合干系,即,让他们彼此都不知道对方是谁甚至不知道对方的范例信息。
这个要领可行么?Yes!
4 桥式委托(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就可以了。这也可以算是一种局部隐含的担任吧。
接下来我们要接头一下这个桥式委托的机能耗损以及扩展和范围性问题
5 桥式委托的进一步研究
看过上面的桥式委托之后,大概会有点猜疑他的机能,需要一个interface指针一个functor类/函数指针,挪用的时候需要一次查vtable,然后再一次做operator()挪用。其实,这些耗损都不算很大的,整个桥式委托的类布局是简朴的,相对付前面说的担任整个类之类的做法开销照旧较量小的,并且又比函数指针通用并且范例安详。最重要的是,适才的Signal可以利便地改写为Multi-Cast Delegation即一个信号激发多个响应——把Singal内部的DelegationInterface*指针改为一个指针行列就可以了;-)
#p#分页标题#e#
不外,我们适才实现的桥式委托只能吸收函数指针和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系统了。
wangtianxing老大点评:用signal的步伐确实可以利便地震态替换处理惩罚函数,不外这是以每个大概被处理惩罚的动静都要在每个工具中占用一个 signal 的空间为价钱的。并且,需要动态改变处理惩罚函数的应用我已经不记得什么时候见过了。纵然有,也可以通过在override的virtual函数里本身处理惩罚实现,虽说贫苦,但也是大概的。另外,以上代码并不足类型,下划线加大写字母开头的标识符是保存给语言的实现用的。
6 结论
好了,我们关于桥式委托的接头靠近尾声了,各人也许已经发明白一个庞大的问题:上面的桥式委托无法给相应操纵通报参数!!!是的,这是一个庞大的抵牾——你必需本身实现带一个参数的桥、本身实现带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;
......
}
哇~~~ 好悲凉! 要本身写这么多桥啊?C++语法这么不给体面……虽然了,你可以绕路来实现,好比用一个通用的打包参数来包装多个参数,用宏界说来处理惩罚各类环境,虽然也可以用预处理惩罚来实现——我这里要说的,同情一下QT吧,不要成天诉苦他的signal/slot体系需要预处理惩罚是在扩展语言——设身处地地想一想,C++提供应我们的就这有这些了,一个小小的参数是我们这些signal/slot抹不去的伤痛。
幸运的是,在C++尺度委员会不绝的尽力之下,这些环境开始有所改进。boost库之中的signal库可以直接支持可变参数的委托;同时,越来越多的元语言技能也引入了C++之中。固然今朝支持这些新特性的编译器还较量少,不外这已经长短常庞大的进步了,让我们等候吧……