boost源码剖析之:多重回调机制signal(下)
刘未鹏
C++的罗浮宫(http://blog.csdn.net/pongba)
在本文的上篇中,我们大刀阔斧的剖析了signal的架构。不过还有很多精微之处没有提到,特别是一个遗留问题还没有解决:如果用户注册的是函数对象(仿函数),signal又当如何处理呢?
下篇:高级篇
概述
在本文的上篇中,我们已经分析了signal的总体架构。至于本篇,我们则主要集中于将函数对象(即仿函数)连接到signal的来龙去脉。signal库的作者在这个方面下了很多功夫,甚至可以说,并不比构建整个signal架构的功夫下得少。
之所以为架构,其中必然隐藏着一些或重要或精妙的思想。
学过STL的人都知道,函数对象[1](function object)是STL中的重要概念和基石之一。它使得一个对象可以像函数一样被“调用”,而调用形式又是与函数一致的。这种一致性在泛型编程中乃是非常重要的,它意味着“泛化”,而这正是泛型世界所有一切的基础。而函数对象又由于其携带的信息较之普通函数大为丰富,从而具有更为强大的能力。
所以signal简直是“不得不”支持函数对象。然而函数对象又和普通函数不同:函数对象会析构。问题在于:如果某个函数对象连接到signal,那么,该函数对象析构时,连接是否应该断开呢?这个问题,signal的设计者留给用户来选择:如果用户觉得函数对象一旦析构,相应的连接也应该自动断开,则可以将其函数对象派生自boost::signals::trackable类,意即该对象是“可跟踪”的。反之则不用作此派生。这种跟踪对象析构的能力是很有用的,在某些情况下,用户需要这种语义:例如,一个负责数据库访问及更新的函数对象,而该对象的生命期受某个管理器的管理,现在,将它连接到某个代表用户界面变化的signal,那么,当该对象的生命期结束时,对应的连接显然应该断开——因为该对象的析构意味着对应的数据库不再需要更新了。
signal库支持跟踪函数对象析构的方式很简单,只要将被跟踪的函数对象派生自boost::signals::trackable类即可,不需要任何额外的步骤。解剖这个trackable类所隐藏的秘密正是本文的重点。
架构
很显然,trackable类是整个问题的关键。将函数对象派生自该类,就好比为函数对象安上了一个“跟踪器”。根据C++语言的规则,当某个对象析构时,先析构派生层次最高(most derived)的对象,再逐层往下析构其子对象。这就意味着,函数对象的析构最终将会导致其基类trackable子对象的析构,从而在后者的析构函数中,得到断开连接的机会。那么,哪些连接该断开呢?换句话说,该断开与哪些signal的连接呢?当然是该函数对象连接到的signals。而这些连接则全部保存在一个list里面。下面就是trackable的代码:
class trackable {
typedef std::list<connection> connection_list;
typedef connection_list::iterator connection_iterator;
mutable connection_list connected_signals;
...
}
connected_signals是个list,其中保存的是该函数对象所连接到的signals。只不过是以connection的形式来表示的。这些connection都是“控制性”[2]的,一旦析构则自动断开连接。所以,trackable析构时根本不需要任何额外的动作,只要让该list自行析构就行了。
了解了这一点,就可以画出可跟踪的函数对象的基本结构,如图四:
图四
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 204.75pt; HEIGHT: 233.25pt" type="#_x0000_t75"><imagedata o:title="boost" src="file:///C:%5CDOCUME~1%5Cpongba%5CLOCALS~1%5CTemp%5Cmsohtml1%5C01%5Cclip_image001.gif"></imagedata></shape>
现在的问题是,每当该函数对象连接到一个signal,都会将相应connection的一个副本插入到其trackable子对象的connected_signals成员(一个list)中去。然而,这个插入究竟发生在何时何地呢?
在本文的上篇中曾经分析过连接的过程。对于函数对象,这个过程仍然是一样。不过,当时略过了一些细节,这些细节正是与函数对象相关的。现在一一道来:
如你所知,在将函数(对象)连接到signal时,函数(对象)会先被封装成一个slot对象,slot类的构造函数如下:
slot(const F& f):slot_function(get_invocable_slot(f,tag_type(f)))
{
//一个visitor,用于访问f中的每个trackable子对象
bound_objects_visitor do_bind(bound_objects);
//如果f为函数对象,则访问f中的每一个trackable子对象
visit_each(do_bind,get_inspectable_slot[3](f,tag_type(f)));
//创建一个connection,表示f与该slot的连接,这是为了实现“delayed-connect”
create_connection();
}
bound_objects是slot类的成员,其类型为vector<const trackable*>。可想而知,经过第二行代码“visit_each(...)”的调用,该vector中保存的将是指向f中的各个trackable子对象的指针。
“等等!”你敏锐的发现了一个问题:“前面不是说过,如果用户要让他的函数对象成为可跟踪的,则将该函数对象派生自trackable对象吗?那么,也就是说,如果f是个“可跟踪”的函数对象,那么其中的trackable子对象当然只有一个(基类对象)!但为什么这里bound_objects的类型却是一个vector呢?单单一个trackable*不就够了么?”
在分析这个问题之前,我们先来看一段例子代码:
struct S1:boost::signals::trackable
{//该对象是可跟踪的!但并非一个函数对象
void test(){cout<<"test\n";}
};
...
boost::signal<void()> sig;
{ //一个局部作用域
S1 s1;
sig.connect(boost::bind(&S1::test,boost::ref(s1)));
sig(); //输出 “test”
} //结束该作用域,s1在此析构,断开连接
sig(); //无输出
boost::bind()将&S1::test[4]的“this”参数绑定为s1,从而生成一个“void()”型的仿函数,每次调用该仿函数就相当于调用s1.test(),然而,这个仿函数本身并非可跟踪的,不过,很显然,这里的s1对象一旦析构,则该仿函数就失去了意义,从而应该让连接断开。所以,我们应该使S1类成为可跟踪的(见struct S1的代码)。
然而,这又能说明什么呢?仍然只有一个trackable子对象!但是,答案已经很明显了:既然boost::bind可以绑定一个参数,难道不能绑定两个参数?对于一个延迟调用的函数对象[5],一旦其某个按引用语义传递的参数析构了,该函数对象也就相应失效了。所以,对于这种函数对象,其按引用传递的参数都应该是可跟踪的。在上例中,s1就是一个按引用传递的参数[6],所以是可跟踪的。所以,如果有多个这种参数绑定到一个仿函数,就会有多个trackable对象,其中任意一个对象的析构都会导致仿函数失效以及连接的断开。
例如,假设C1,C2类都是trackable的。并且函数test的类型为void(C1,C2)。那么boost::bind(&test,boost::ref(c1),boost::ref(c2))就会返回一个void()型的函数对象,其中c1,c2作为test的参数绑定到了该函数对象。这时候,如果c1或c2析构,这个函数对象也就失效了。如果先前该函数对象曾连接到某个signal<void()>型的signal,则连接应该断开。
问题在于,如何获得绑定到某个函数对象的所有trackale子对象呢?
关键在于visit_each函数——我们回到slot的构造函数(见上文列出的源代码),其第二行代码调用了visit_each函数,该函数负责访问f中的各个trackable子对象,并将它们的地址保存在bound_objects这个vector中。
至于visit_each是如何访问f中的各个trackable子对象的,这并非本文的重点,我建议你自行参考源代码。
slot类的构造函数最后调用了create_connection函数,这个函数创建一个连接对象,表示函数对象和该slot的连接。“咦?为什么和slot连接,函数对象不是和signal连接的吗?”没错。但这个看似蛇足的举动其实是为了实现“delayed connect”,例如:
void delayed_connect(Functor* f)
{
//构造一个slot,但暂时不连接
slot_type slot(*f);
//使用f做一些事情,在这个过程中f可能会被析构掉
...
//如果f已经被析构了,则slot变为inactive态,则下面的连接什么事也不做
sig.connect(slot);
}
...
Functor* pf=new Functor();
delayed_connect(pf);
...
这里,如果在slot连接到sig之前,f“不幸”析构了,则连接不会生效,只是返回一个空连接。
为了达到这个目的,slot类的构造函数使用create_connection构造一个连接,这个连接其实没有实际意义,只是用于“监视”函数对象是否析构。如果函数对象析构了,则该连接会变为“断开”态。下面是create_connection的源代码:
摘自libs/signals/src/slot.cpp
void slot_base::create_connection()
{
basic_connection* con = new basic_connection();
font-size: 12p
评论