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 的内部架构,如果给出它的总体轮廓,非常清晰明了。见下图:
图一
<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: 96.75pt; HEIGHT: 177.75pt" type="#_x0000_t75"><imagedata o:title="boost" src="file:///C:/DOCUME~1/pongba/LOCALS~1/Temp/msohtml1/01/clip_image001.gif"></imagedata></shape>
显然, 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 的架构可以这样表示:
图二
<shape id="_x0000_i1026" style="WIDTH: 293.25pt; HEIGHT: 276.75pt" type="#_x0000_t75"><imagedata o:title="boost" src="file:///C:/DOCUME~1/pongba/LOCALS~1/Temp/msohtml1/01/clip_image002.gif"></imagedata></shape>
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 对象,后者唯一表现了这个连接。经过再次精化后的架构图如下:
图三
<shape id="_x0000_i1027" style="WIDTH: 381pt; HEIGHT: 157.5pt" type="#_x0000_t75"><imagedata o:title="boost" src="file:///C:/DOCUME~1/pongba/LOCALS~1/Temp/msohtml1/01/clip_image003.gif"></imagedata></shape>
这样,当用户通过其中任意一个 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 源码剖析》系列 文章 )
[2] boost 库的源代码可从 sf.boost.org 网站获得。目前的版本是 <chsdate w:st="on" year="1899" month="12" day="30" islunardate="False" isrocdate="False"><span lang="EN-US" style="COLOR: black"><font face="Times New Roman">1.31.0</font></span></chsdate> 。
[4] boost::function 是个泛型的函数指针类,例如 boost::function<int(int,double)> 可以指向任何类型为 int(int,double) 的函数或仿函数。
[6] 顾名思义,即带有共享计数的指针, boost 库里面有很多种智能指针,此其一
[7] slots_ 是 signal_base_impl 中的唯一的数据成员,也就是 slot 管理器。请回顾前面的源代码。
[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) 。