`
61party
  • 浏览: 1056201 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

boost源码剖析之:多重回调机制signal(下)

阅读更多

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_objectsslot类的成员,其类型为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的参数绑定到了该函数对象。这时候,如果c1c2析构,这个函数对象也就失效了。如果先前该函数对象曾连接到某个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

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics