boost源码剖析之:多重回调机制signal(上)
刘未鹏
C++的罗浮宫(http://blog.csdn.net/pongba)
boost库固然是技术的宝库,却更是思想的宝库。大多数程序员都知道如何应用command,observer等模式,却不知该如何写一个支持该模式的类。正如隔靴搔痒,无法深入。DDJ上曾有一篇文章用C++实现类似C#的event机制,不过是个雏形,比之boost.Signal却又差之甚远矣。
上篇:架构篇
引入
所谓“事件”机制,简而言之,就是用户将自己的一个或多个回调函数挂钩到某个“事件”上,一旦“事件”被触发,所有挂钩的函数都被调用。
毫无疑问,事件机制是个十分有用且常用的机制,不然C#也不会将它在语言层面实现了。
但是C++语言并无此种机制。
幸运的是boost库的开发者们替我们做好了这件事(事实上,他们做的还要更多些)。他们的类称作signal,即“信号”的意思,当“信号”发出的时候,所有注册过的函数都将受到调用。这与“事件”本质上完全一样。
简单情况下,你只需要这样写:
double square(double d){return pi*r*r;} //面积
double circle(double d){return 2*pi*r;} //周长
//double(double)是一个函数类型,意即:接受一个double型参数,返回double。
signal<double(double)[1]> sig;
sig.connect(&square); //向sig注册square
sig.connect(&circle);//注册circle
//触发该信号,sig会自动调用square(3.14),circle(3.14),并返回最后一个函数,circle()的返回值
double c=sig(3.14); //assert(c==circle(3.14))
signal能够维护一系列的回调函数,并且,signal还允许用户指定函数的调用顺序,signal还允许用户定制其返回策略,默认情况下返回(与它挂钩的)最后一个函数的返回值,当然你可以指定你自己的“返回策略”(比如:返回其中的最大值),其中手法,甚为精巧。另外,如果注册的是函数对象(仿函数)而非普通函数,则signal还提供了跟踪能力,即该函数对象一旦析构,则连接自动断开,其实现更是精妙无比。
俗语云:“熟读唐诗三百首,不会吟诗也会吟”。写程序更是如此。如果仔细体会,会发现signal的实现里面隐藏了许许多多有价值的思想和模式。何况boost库是个集泛型技术之大成的库,其源代码本身就是一笔财富,对于深入学习C++泛型技术是极好的教材。所以本文不讲应用,只讲实现,你可以边读边参照boost库的源代码[2]。另外,本文尽量少罗列代码,多分析架构和思想,并且列出的代码为了简洁起见,往往稍作简化[3],略去了一些细节,但是都注明其源文件,自行参照。
在继续往下读之前,建议大家先看看boost库的官方文档,了解signal的各种使用情况,这样,在经历下面繁复的分析过程时心中才会始终有一个清晰的脉络。事实上,我在阅读代码之前也是从各种例子入手的。
架构
Signal的内部架构,如果给出它的总体轮廓,非常清晰明了。见下图:
图一
显然,signal在内部需要一个管理设施来管理用户所注册的函数(这就是图中的slot manager),从根本上来说,boost::signal中的这个slot“管理器”就是multimap(如果你不熟悉multimap,可以参考一些STL方面的书籍(如《C++ STL》《泛型编程与STL》)或干脆查询MSDN。这里我只简单的说一下——multimap将键(key)映射(map)到键值(键和键值的类型可以是任意),就像字典将字母映射到页码一样。)它负责保存所谓的slot,每一个slot其实本质上是一个boost::function[4]函数对象,该函数对象封装了用户注册给signal回调的函数(或仿函数)。当然,slot是经过某种规则排序的。这正是signal能够控制函数调用顺序的原因。
当你触发signal时,其内部迭代遍历“管理器”——multimap,找出其中保存的所有函数或函数对象并逐一调用它们。
听起来很简单,是不是?但是我其实略去了若干细节,譬如,如何让用户控制某个特定的连接?如何控制函数的调用顺序?如何实现可定制的返回策略?等等。
看来设计一个“industry-strength”的signal并非一件易事。事实上,非常不易。然而,虽然我们做不到,却可以看看大师们的手笔。
我们从signal的最底层布局开始,signal的底层布局十分简单,由一个基类signal_base_impl来实现。下面就是该基类的代码:
摘自boost/signals/detail/signal_base.hpp
class signal_base_impl {
public:
typedef function2<bool, any, any> compare_type;
private:
typedef std::multimap<any, connection_slot_pair, compare_type> slot_container_type; //以multimap作为slot管理器的类型
//遍历slot容器的迭代器类型
typedef slot_container_type::iterator slot_iterator;
//slot容器内部元素的类型,事实上,那其实就是std::pair<any,connection_slot_pair>。
typedef slot_container_type::value_type stored_slot_type;
//这就是slot管理器,唯一的数据成员——一个multimap,负责保存所有的slot。
mutable slot_container_type slots_;
...
};
可以看出slot管理器的类型是个multimap,其键(key)类型却是any[5],这是个泛型的指针,可以指向任何对象,为什么不是整型或其它类型,后面会为你解释。
以上是主要部分,你可能会觉得奇怪,为什么保存在slot管理器内部的元素类型是个怪异的connection_slot_pair而不是boost::function,前面不是说过,slot本质上就是boost::function对象么?要寻求答案,最好的办法就是看看这个类型定义的代码,源代码会交代一切。下面就是connection_slot_pair的定义:
摘自boost/signals/connection.hpp
struct connection_slot_pair {
//connection类用来表现“连接”这个概念,用户通过connection对象来控制相应的连接,例如,调用成员函数disconnect()则断开该连接
connection first;
//any是个泛型指针类,可以指向任何类型的对象
any second;
//封装用户注册的函数的boost::function对象实际上就由这个泛型指针来持有
...
};
原来,slot管理器内部的确保存着boost::function对象,只不过由connection_slot_pair里的second成员——一个泛型指针any——来持有。并且,还多出了一个额外的connection对象——很显然,它们是有关联的——connection成员表现的正是该function与signal的连接。为什么要多出这么一个成员呢?原因是这样的:connection一般掌握在用户手中,代码象这样:
connection con=sig.connect(&f); // 通过con来控制这个连接
而signal如果在该连接还没有被用户断开(即用户还没有调用con.disconnect())前就析构了,自然要将其中保存的所有slot一一摧毁,这时候,如果slot管理器内部没有保存connection的副本,则slot管理器就无法对每个slot一一断开其相应的连接,从而控制在用户手中的connection对象就仿佛一个成了一个野指针,这是件很危险的事情。从另一个方面说,既然slot管理器内部保存了connection的副本,则只要让这些connection对象析构的时候能自动断开连接就行了,这样,即使用户后来还试图断开手里的con连接,也能够得知该连接已经断开了,不会出现危险。有关connection的详细分析见下文。
根据目前的分析,signal的架构可以这样表示:
图二
boost::signals::connection类
connection类是为了表现signal与具体的slot之间的“连接”这种概念。signal将slot安插妥当后会返回一个connection对象,用户可以持有这个对象并以此操纵与它对应的“连接”。而每个slot自己也和与它对应的connection呆在一起(见上图),这样slot管理器就能够经由connection_slot_pair中的first元素来管理“连接”,也就是说,当signal析构时,需要断开与它连接的所有slot,这时就利用connection_slot_pair中的first成员来断开连接。而从实际上来说,slot管理器在析构时却又不用作任何额外的工作,只需按部就班的析构它的所有成员(slot)就行了,因为connection对象在析构时会考虑自动断开连接(当其内部的is_controlling标志为true时)。
要注意的是,对于同一个连接可能同时存在多个connection对象来表现(和控制)它,但始终有一个connection对象是和slot呆在一起的,以保证在signal析构时能够断开相应的连接,其它连接则掌握在用户手中,并且允许拷贝。很显然,一旦实际的连接被某个connection断开,则对应于该连接的其它connection对象应该全部失效,但是库的设计者并不知道用户什么时候会拷贝connection对象和持有多少个connection对象,那么用户经过其中一个connection对象断开连接时,其它connection对象又是如何知道它们对应的连接是否已经断开呢?原因是这样的:对于某个特定连接,真正表现该连接的只有唯一的一个basic_connection对象。而connection对象其实只是个外包类,其中有一个成员是个shared_ptr[6]类型的智能指针,从而对应于同一个连接的所有connection对象其实都通过这个智能指针指向同一个basic_connection对象,后者唯一表现了这个连接。经过再次精化后的架构图如下:
图三
这样,当用户通过其中任意一个connection对象断开连接(或signal通过与slot保存在一块的connection对象断开连接)时,connection对象只需转交具体表现该连接的唯一的basic_connection对象,由它来真正断开连接即可。这里,需要注意的是,断开连接并非意味着唯一表示该连接的basic_connection对象的析构。前面已经讲过,connection类里有一个shared_ptr智能指针指向basic_connection对象,所以,当指向basic_connection的所有connection都析构掉后,智能指针自然会将basic_connection析构。其实更重要的原因是,从逻辑上,basic_connection还充当了信息中介——由于控制同一连接的所有connection对象都共享它,从而都可以查看它的状态来得知连接是否已经断开,如果将它delete掉了,则其它connection就无从得知连接的状态了。所以这种设计是有良苦用心的。正因此,一旦某个连接被断开,则对应于它的所有connection对象都可得知该连接已经断开了。
对于connection,还有一个特别的规则:connection对象分为两种,一种是“控制性”的,另一种是“非控制性”的。掌握在用户手中的connection对象为“非控制性”的,也就是说析构时不会导致连接的断开——这符合逻辑,因为用户手中的connection对象通常只是暂时的复制品,很快就会因为结束生命期而被析构掉,况且,signal::connect()返回的connection对象也是临时对象,用户可以选择丢弃该返回值(即不用手动管理该连接),此时该返回值会立即析构,这当然不应该导致连接的断开,所以这种connection对象是“非控制性”的。而保存在slot管理器内部,与相应的slot呆在一起的connection对象则是“控制性”的,一旦析构,则会断开连接——这是因为它的析构通常是由signal对象的析构导致的,所谓“树倒猢狲散”,signal都不存在了,当然要断开所有与它相关的连接了。
了解了这种架构,我们再来跟踪一下具体的连接过程。
连接
向signal注册一个函数(或仿函数)甚为简单,只需调用signal::connect()并将该函数(或仿函数)作为参数传递即可。不过,要注意的是,注册普通函数时需提供函数的地址才行(即“&f”),而注册函数对象时只需将对象本身作为参数。下面,我们从signal::connect()开始来跟踪signal的连接过程。
前提:下面跟踪的全过程都假设用户注册的是普通函数,这样有助于先理清脉络,至于注册仿函数(即函数对象)时情况如何,将在高级篇中分析。
源代码能够说明一切,下面就是signal::connect()的代码:
template<...>
connection signal<...>::connect(const slot_type& in_slot)
{...}
这里,我们先不管connect()函数内部是如何运作的,而是集中于它的唯一一个参数,其类型却是const slot_type&,这个类型其实对用户提供的函数(或仿函数)进行一重封装——封装为一个“slot”。至于为什么要多出这么一个中间层,原因只是想提供给用户一个额外的自由度,具体细节容后再述。
slot_type其实只是一个位于signal类内部的typedef,其真实类型为slot类。
很显然,这里,slot_type的构造函数将被调用(参数是用户提供的函数或仿函数)以创建一个临时对象,并将它绑定到这个const引用。下面就是它的构造函数:
template<typename F>
slot(const F& f) : slot_function(f)
{
... //这里,我们先略过该构造函数里面的代码(后面再回顾)
}
可以看出,用户给出的函数(或仿函数)被封装在slot_function成员中,slot_function的类型其实是boost::function<...>,这是个泛型的函数指针,封装任何签名兼容的函数及仿函数。将来保存在slot管理器内部的就是它。
下面,slot临时对象构造完毕,仍然回到signal::connect()来:
摘自boost/signals/signal_template.hpp
connection signal<...>::connect(const slot_type& in_slot)
{
...
return impl->connect_slot(in_slot.get_slot_function(),
any(),
in_slot.get_bound_objects());
}
这里,signal将一切又交托给了其基类的connect_slot()函数,并提供给它三个参数,注意,第一个参数in_slot.get_slot_function()返回的其实正是刚才所说的slot类的成员slot_function,也正是将要保存在slot管理器内部的boost::function对象。而第二个参数表示该用户注册函数的优先级,
signal::connect()其实有两个重载版本,第一个只有一个参数,就是用户提供的函数,第二个却有两个参数,其第一个参数为优先级,默认是一个整数。这里,我们考察的是只有一个参数的版本,意味着用户不关心该函数的优先级,所以默认构造一个空的any()对象(回忆一下,slot管理器的键(key)类型为any)。至于第三个参数仅在用户注册函数对象时有用,我们暂时略过,在高级篇里再详细叙述。现在,继续追踪至connect_slot()的定义:
摘自libs/signals/src/signal_base.cpp
connection
signal_base_impl::
connect_slot(const any& slot,
const any& name,
const std::vector<const trackable*>& bound_objects)
//最后一个参数当用户提供仿函数时方才有效,容后再述
{
//创建一个basic_connection以表现本连接——注意,一个连接只对应于一个basic_connection对象,但可以有多个connection对象来操纵它。具体原因上文有详述。
basic_connection* con = new basic_connection();
connection slot_connection;
slot_connection.reset(con);
std::auto_ptr<slot_iterator> saved_iter(new slot_iterator());
//用户注册的函数在此才算真正在signal内部安家落户——即将它插入到slot管理器(multimap)中去
slot_iterator pos =
slots_.insert(stored_slot_type(name,
connection_slot_pair(slot_connection,slot)
));
//保存在slot管理器内部的connection对象应该设为“控制性”的。具体原因上文有详述。
pos->second.first.set_controlling();
*saved_iter = pos;
//下面设置表现本连接的basic_connection对象的各项数据,以便管理该连接。
con->signal = this; //指向连接到的signal
con->signal_data = saved_iter.release();//一个iterator,指出回调函数在signal中的slot管理器中的位置
con->signal_disconnect = &signal_base_impl::slot_disconnected; //如果想断开连接,则应该调用此函数,并将前面两项数据作为参数传递过去,则回调函数将被从slot管理器中移除。
...
return slot_connection;//返回该连接
}
这个函数结束后,连接也就创建完了,看一看最后一行代码,正是返回该连接。
从上面的代码可以看出,basic_connection对象有三个成员:signal,signal_data,signal_disconnect,这三个成员起到了控制该连接的作用。源代码上的注释已经提到,成员signal指向连接到的是哪个signal。而signal_data其实是个iterator,指明了该slot在slot管理器中的位置。最后,成员signal_disconnect则是个void(*)(void*,void*)型的函数指针,指向一个static成员函数——signal_base_impl::slot_disconnected。以basic_connection中的signal和signal_data两个成员作为参数来调用这个函数就能够断开该连接。即:
(*signal_disconnect)(local_con->signal, local_con->signal_data);
然而,具体如何断开连接还得看slot_disconnected函数的代码(注意将它和上面的connect_slot函数的代码作一个比较,它们是几乎相反的过程)
摘自libs/signals/src/signal_base.cpp
void signal_base_impl::slot_disconnected(void* obj, void* data)
{
signal_base_impl* self = reinterpret_cast<signal_base_impl*>(obj);//指明连接到的是哪个signal
//指出slot在slot管理器中的位置
std::auto_ptr<slot_iterator> slot(
reinterpret_cast<slot_iterator*>(data));
... //省略部分代码。
self->slots_.erase(*slot);//将相应的slot从slot管理器中移除
}
值得注意的是,basic_connection中的两个成员:signal和signal_data的类型都是void*,具体原因在高级篇里会作解释。而slot_disconnected函数的代码不出所料:先将两个参数的类型转换为合适的类型,还其本来面目:一个是signal_base_impl*,另一个是指向迭代器的指针:slot_iterator*,然后调用slots_[7]上的erase函数将相应的slot移除,就算完成了这次disconnect。这简直就是connect_slot()的逆过程。
这里,你可能会有疑问:这样就算断开了连接?那么用户如果不慎通过某个指向该basic_connection的connection再次试图断开连接又当如何呢?更可能的情况是,用户想要再次查询该连接是否断开。如此说来,basic_connection中是否应该有一个标志,表示该连接是否已断开?完全不必,其第三个成员signal_disconnect是个函数指针,当断开连接后,将它置为0,不就是个天然的标志么?事实上,connection类的成员函数connected()就是这样查询连接状态的:
摘自boost/signals/connection.hpp
bool connected() const
{
return con.get() && con->signal_disconnect;
}
再次提醒一下,con是个shared_ptr,指向basic_connection对象。并且,尤其要注意的是,连接断开后,表示该连接的basic_connection对象并不析构,也不能析构,因为它还要充当连接状态的标志,以供仍可能在用户手中的connection对象来查询。当指向它的所有connection对象都析构时,根据shared_ptr的规则,它自然会析构掉。
好了,回到主线,连接和断开连接的大致过程都已经分析完了。其中我略去了很多技术细节,尽量使过程简洁,这些技术细节大多与仿函数有关——假若用户注册的是个仿函数,就有得折腾了,其中曲折甚多,我会在高级篇里详细分析。
排序
跟踪完了连接过程,下面是真正的调用过程,即触发了signal,各个注册的函数均获得一次调用,这个过程逻辑上颇为简单:从slot管理器中将它们一一取出并调用一次不就得了?但是,正如前面所说的,调用可是要考虑顺序的,各个函数可能有着不同的优先级,这又该如何管理呢?问题的关键就在于multimap的排序,一旦将函数按照用户提供的优先级排序了,则调用时只需依次取出调用就行了。那么,排序准则是什么呢?如你所知,一个signal对象sig允许注册这样一些函数:
sig.connect(&f0); //f0没有优先级
sig.connect(1,&f1);//f1的优先级为1
sig.connect(2,&f2); //f2的优先级为2
sig.connect(&f3); //f3没有优先级
这时候,这四个函数的顺序是f1,f2,f0,f3。准则这样的,如果用户为某个函数提供了一个优先级,如1,2等,则按优先级排序,如果没有提供,则相应函数追加在当前函数队列的尾部。这样的排序准则如何实现呢,很简单,只需要将一个仿函数提供给multimap来比较它的键,multimap自己会排序妥当,这个仿函数如下:
摘自boost/signals/detail/signal_base.hpp
template<typename Compare, typename Key>
class any_bridge_compare {
...
//slot管理器的键类型为any,所以该仿函数的两个参数类型都是any
bool operator()(const any& k1, const any& k2) const
{
//如果k1没有提供键(如f0,它的键any是空的)则它处于任何键之后
if (k1.empty())
return false;
//如果k2没有提供键,则任何键都排在它之前
if (k2.empty())
return true;
//如果两个键都存在,则将键类型转换为合适的类型再作比较
return comp(*any_cast<Key>(&k1), *any_cast<Key>(&k2));
}
private:
Compare comp;
};
这个仿函数就是提供给slot管理器来将回调函数排序的仿函数。它的比较准则为:首先看k1是否为空,如果是,则在任何键之后。再看k2是否为空,如果是,则任何键都在它之前。否则,如果两者都非空,则再另作比较。并且,从代码中看出,这最后一次比较又转交给了Compare这个仿函数,并事先将键转型为Key类型(既然非空,就可以转型了)。Key和Compare这两个模板参数都可由用户定制,如果用户不提供,则为默认值:Key=int,Compare=std::less<int>。
现在你大概已经明白为什么slot管理器要以any作为其键(key)类型了,正是为了实现“如果用户不指定优先级,则优先级最低”的语义。试想,如果用户指定什么类型,slot管理器的键就是什么类型——如int,那么哪个值才能表示“最低优先级”这个概念呢?正如int里面没有值可以表现“负无穷大”的概念一样,这是不可能的。但是,如果用一个指针来指向这个值,那么当指针空着的时候,我们就可以说“这是个特殊的值”,本例中,这个特殊值就代表“优先级最低”,而当指针非空时,我们再来作真正的比较。况且,any是个特殊的指针,你可以以类型安全的方式(通过一个any_cast<>)从中取出你先前保存的任何值(如果类型不符,则会抛出异常)。
回顾上面的例子,对于f0,f3没有提供相应的键,从而构造了一个空的any()对象,根据前面所讲的比较准则,其“优先级最低”,并且,由于f3较晚注册,所以在最末端(想想前面描述的比较准则)。
当然,用户也可以定制
Key=std::string,
Compare=std::greater<std::string>。
总之一切按你的需求。
回调
下面要分析的就是回调了。回调函数已经连接到signal,而触发signal的方式很简单,由于signal本身就是一个函数对象,所以可以这样:
signal<int(int,double)> sig;
sig.connect(&f1);
sig.connect(&f2);
int ret=sig(0,3.14); //正如调用普通函数一样
前面提到过,signal允许用户定制其返回策略(即,返回最大值,或最小值等),默认情况下,signal返回所有回调函数的返回值中的最后一个值,这通过一个模板参数来实现,在signal的模板参数中有一个名为Combiner,是一个仿函数,默认为:
typename Combiner = last_value<R>
last_value是个仿函数,它有两个参数,均为迭代器,它从头至尾遍历这两个迭代器所表示的区间,并返回最后一个值,算法定义如下:
摘自boost/last_value.hpp
T operator()(InputIterator first, InputIterator last) const
{
T value = *first++;
while (first != last)
value = *first++;
return value;
}
我本以为signal会以一个简洁的for_each遍历slot管理器,辅以一个仿函数来调用各个回调函数,并将它们的返回值缓存为一个序列,而first和last正指向该序列的头尾。然后在该序列上应用该last_value算法(返回策略),从而返回恰当的值。这岂非很自然?
但是很明显,将各个回调函数的返回值缓存为一个序列需要消耗额外的空间和时间,况且我在signal的operator()操作符的源代码里只发现一行!就是将last_value应用于一个区间。在此之前找不到任何代码是遍历slot管理器并一一调用回调函数的。但回调函数的确被一一调用了,只不过方式很巧妙,也很隐藏,并且更简洁。继续往下看。
从某种程度上说,参数first指向slot管理器(multimap)的区间头,而last指向其尾部。但是,既然该仿函数名为last_value,那么直接返回*(--last)岂不更省事?为何非要在区间上每前进一步都要对迭代器解引用呢(这很关键,后面会解释)?况且,函数调用又在何处呢?slot管理器内保存的只不过是一个个函数,遍历它,取出函数又有何用?问题的关键在于,first并非单纯的只是slot管理器的迭代器,而是一个iterator_adapter,也就是说,它将slot管理器(multimap)的迭代器封装了一下,从而对它解引用的背后其实调用了函数。有点迷惑?接着往下看:
iterator_facade(iterator_adapter)
iterator_facade(iterator_adapter)在boost库里面是一个独立的组件,其功能是创建一个具有iterator外观(语义)的类型,而该iterator的具体行为却又完全可以由用户自己定制。具体用法请参考boost库的官方文档。这里我们只简单描述其用途。
上面提到,传递给last_value<>仿函数的两个迭代器是经过封装的,如何封装呢?这两个迭代器的类型为slot_call_iterator,这正是个iterator_adapter,其代码如下:
摘自boost/signals/detail/slot_call_iterator.hpp
template<typename Function, typename Iterator>
class slot_call_iterator //参数first的类型其实是这个
:public iterator_facade<...>
{
...
dereference() const
{
return f(*iter); //调用iter所指向的函数
}
};
iterator_facade是个模板类,其中定义了迭代器该有的一切行为如:operator ++,operator --,operator *等,但是具体实施该行为的却是其派生类(这里为slot_call_iterator),因为iterator_facade会将具体动作转交给其派生类来执行,比如,operator*()在iterator_facade中就是这样定义的:
reference operator*() const
{
//转而调用派生类的dereference()函数
return this->derived().dereference();
}
而派生类的dereference()函数在前面已经列出了,其中只有一行代码:return f(*iter),iter自然是指向slot管理器内部的迭代器了,*iter返回的值当然是connection_slot_pair[8],下面只需要取出这个pair中的second成员[9],然后再调用一下就行了。但是为什么这里的代码却是f(*iter),f是个什么东东?在往下跟踪会发现,事实上,f保存了触发signal时提供的各个参数(在上面的例子中,是0和3.14)而f其实是个仿函数,f(*iter)其实调用了它重载的operator(),后者才算完成了对slot的真正调用,代码如下:
摘自boost/signals/signal_template.hpp:
R operator()(const Pair& slot) const
{
F* target = const_cast<F*>(any_cast<F>(&slot.second.second[10]));
return (*target)(args->a1,args->a2);//真正的调用在这里!!!
}
这两行代码应该很好理解:首先取出保存在slot管理器(multimap)中的function(通过一个any_cast<>),然后调用它,并将返回值返回。
值得说明的是,args是f的成员,它是个结构体,封装了调用参数,对于本例,它有两个成员a1,a2,分别保存的是signal的两个参数(0和3.14)。而类型F对于本例则为boost::function<int(int,double)>[11],这正是slot管理器内所保存的slot类型,前面已经提到,这个slot由connection_pair里面的second(一个any类型的泛型指针)来持有,所以这里出现了any_cast<>,以还其本来面目。
所以说,slot_call_iterator这个迭代器的确是在遍历slot管理器,但是对它解引用其实就是在调用当前指向的函数,并返回其返回值。了解到这一点,再回顾一下last_value的代码,就不难理解为什么其算法代码中要步步解引用了——原来是在调用函数!
简而言之,signal的这种调用方式是“一边迭代一边调用一边应用返回策略”,三管齐下。
“这太复杂了”你抱怨说:“能不能先遍历slot管理器,依次调用其内部的回调函数,然后再应用返回策略呢?”。答案是当然能,只不过如果那样,就必须先将回调函数的返回值缓存为一个序列,这样才能在其上应用返回策略。哪有三管齐下来得精妙?
现在,你可以为signal定制返回策略了,具体的例子参考libs/signals/test/signal_test.cpp。
后记
本文我们只分析了signal的大致架构。虽然内容甚多,但其实只描述了signal的小部分。其中略去了很多技术性的细节,例如slot管理器内保存的函数对象为什么要用any来持有。而不直接为function<...>,还有slot管理器里的调用深度管理——即如果某个回调函数要断开自身与signal的连接该如何处理。还有,对slot_call_iterator解引用时其实将函数调用的返回值缓存了起来(文中列出的代码为简单起见,直接返回了该返回值),如何缓存,为什么要缓存?还有,为什么basic_connection中的signal和signal_data成员的类型都是void*?还有signal中所用到的种种泛型技术等等。
当然,细节并非仅仅是细节,很多精妙的东西就隐藏在细节中。另外,我们没有分析slot类的用处——不仅仅作为中间层。最后,一个最大的遗留问题是:如果注册的是函数对象,如何跟踪其析构,这是个繁杂而精妙的过程,需要篇幅甚多。
这些都会在下篇——高级篇中一一水落石出。
目录(展开《boost源码剖析》系列文章)
[1] double(double)是个类型,函数类型。所谓函数类型可以看作将函数指针类型中的’(*)’去掉后得到的类型。事实上,函数类型在面临拷贝语义的上下文中会退化为函数指针类型。
[2] boost库的源代码可从sf.boost.org网站获得。目前的版本是 1.31.0 。
[3] boost库的源代码里面宏和泛型用得极多,的确很难读懂,所以本文中列出的代码都是简单起见,只取出其中最关键的部分。所有的宏都已展开,所以,有些代码与boost库里的源代码有些不同。
[4] boost::function是个泛型的函数指针类,例如boost::function<int(int,double)>可以指向任何类型为int(int,double)的函数或仿函数。
[5] boost::any也是boost库的一员。它是个泛型指针,可以指向任意对象,并可以从中以类型安全的方式取出保存的对象。
[6] 顾名思义,即带有共享计数的指针,boost库里面有很多种智能指针,此其一
[7] slots_是signal_base_impl中的唯一的数据成员,也就是slot管理器。请回顾前面的源代码。
[8] 这里为了简单起见才这样讲,其实*iter返回的类型是std::pair<any,conection_slot_pair>,any为multimap的键(key)类型。
[9] 前面已经提到过,这个second成员是个any类型的泛型指针,指向的其实就是boost::function,后者封装了用户注册的函数或函数对象。
[10] 前面已经提到了,slot管理器内保存的是个connection_slot_pair,其second成员是any,指向boost::function对象,而slot管理器本身是个multimap,所以其中保存的值类型是std::pair<any,conection_slot_pair>,*iter返回的正是这个类型的值,所以这里需要取两次second成员:slot.second.second。
[11] 再次提醒,boost::function<int(int,double)>可以指向任何类型为int(int,double)的函数或函数对象,本例中,f1,f2,f3,f4函数的类型都是int(int,double)。