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)> 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库的源代码。另外,本文尽量少罗列代码,多分析架构和思想,并且列出的代码为了简洁起见,往往稍作简化,略去了一些细节,但是都注明其源文件,自行参照。
在继续往下读之前,建议大家先看看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:%5CDOCUME~1%5Cpongba%5CLOCALS~1%5CTemp%5Cmsohtml1%5C01%5Cclip_image001.gif"></imagedata></shape>
显然,signal在内部需要一个管理设施来管理用户所注册的函数(这就是图中的slot manager),从根本上来说,boost::signal中的这个slot“管理器”就是multimap(如果你不熟悉multimap,可以参考一些STL方面的书籍(如《C++ STL》《泛型编程与STL》)或干脆查询MSDN。这里我只简单的说一下——multimap将键(key)映射(map)到键值(键和键值的类型可以是任意),就像字典将字母映射到页码一样。)它负责保存所谓的slot,每一个slot其实本质上是一个boost::function函数对象,该函数对象封装了用户注册给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,这是个泛型的指针,可以指向任何对象,为什么不是整型或其它类型,后面会为你解释。
以上是主要部分,你可能会觉得奇怪,为什么保存在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:%5CDOCUME~1%5Cpongba%5CLOCALS~1%5CTemp%5Cmsohtml1%5C01%5Cclip_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类型的智能指针,从而对应于同一个连接的所有connection对象其实都通过这个智能指针指向同一个basic_connection对象,后者唯一表现了这个连接。经过再次精化后的架构图如下:
图三
<shape id="_x0000_i1027" style="WIDTH: 381pt; HEIGHT: 157.5pt" type="#_x0000_t75"><imagedata o:title="boost" src="file:///C:%5CDOCUME~1%5Cpongba%5CLOCALS~1%5CTemp%5Cmsohtml1%5C01%5Cclip_image003.gif"></imagedata></shape>
这样,当用户通过其中任意一个connection对象断开连接(或signal通过与slot保存在一块的connection对象断开连接)时,connection对象只需转交具体表现该连接的唯一的basic_connection对象,由它来真正断开连接即可。这里,需要注意的是,断开连接并非意味着唯一表示该连接的basic_connection</spa
分享到:
相关推荐
通过boost::asio::serialport类实现串口通信的例子
boost::asio::serial下6个工程演示多种串口读取写入方式方法,包含simple,with_timeout,async,callback,qt_integration,stream 等多个工程演示多种方式读取,写入串口,char,string ,buffer[]等多种数据格式。
详细讲述了boost::thread的用法
STL-Boost:STL-Boost源码剖析
boost库,dns解析模块源码。 将其放倒boost库的相关目录下,在代码中,直接包含头文件即可使用。
如果你还在为自己写的程序存在各种野指针,内存泄漏,甚至崩溃的问题而苦恼的话,请使用boost;如果你还在为自己写的程序存在很多与Windows依赖的操作导致无法跨平台而困扰的话,请赶紧采用boost吧. 它将为你提供丰富多样...
boost源码 boost源码 boost源码 boost源码 boost源码 boost源码
第6章Boost深入剖析之使用技巧 《Boost深入剖析之使用技巧》第四讲:Boost容器库(中).flv 如果你还在为自己写的程序存在各种野指针,内存泄漏,甚至崩溃的问题而苦恼的话,请使用boost;如果你还在为自己写的程序存在...
使用VS2017编译的boost库最新版1.68的动态库和静态库,多线程参数,经过测试可用
本发布版的目录树包含了Boost的几乎所有东西:文档、源程序、头文件、脚本、工具,以及一个Boost用户可能需要的所有东西! 欢迎来到 Boost C++ 库 Boost 提供了免费的、对等审查的、可移植的 C++ 源程序库。 我们...
boost::lexical_cast用法示例,包含数值转字串,字串转数值以及相应的异常处理代码
资源名称:C 编程 boost库资料大全资源目录:【】01date_time【】02smart_ptr【】03pool【】04usefultools【】05lexical_cast【】Boost库概览【】Boost程序库完全开发指南【】cboost【】可移植的C 标准库Boost【】用...
using boost::system::error_code; struct logger { void operator ()(error_code ec, message m) { cout << m << endl; } }; void main () { io_service io; dbus::proxy avahi (io, dbus::...
我自己制作的PDF目录,很详细的。 作为C++的准标准库,在C++的开发中的地位已经变得很重要了,自从发现了这本书后,我就开始迫不及待看起来。但是,从网上下载的许多pdf都是没有目录的,要么就是加密了的,所以我...
boost的1.65.1的源码,可以在Linux平台下,编译为二进制,作为第三方库使用。
C++的最新标准(C++11)已经正式公布,而早在这之前,Boost就已经使用库的形式实现了大部分新功能——而且是完全基于C++98标准实现的,内容涵盖智能指针、文本处理、并发、模板元编程等许多领域,其范围之广内涵之深...
boost::asio完成了通讯模块的编写,界面用MFC简单做了一下。 局域网的测试结果: 传输速度在6-7m/s 并发到500,服务器CPU和网络应用均出现使用99%的情况出现硬件瓶颈,新连接无法建立(测试服务器比较差,CPU:...
需要包含boost的路径为: include path: F:\boost_1_53_0 lib path:F:\boost_1_53_0\stage\lib 需要预定义的宏: _WIN32_WINNT=0x0700 在程序里写的接收ip为192.168.1.206,端口为 9002
BOOST程序库完全开发指南:深入...以及boost源码1.71.0 BOOST程序库完全开发指南:深入C++“准”标准库(第3版) 以及boost源码1.71.0 BOOST程序库完全开发指南:深入C++“准”标准库(第3版) 以及boost源码1.71.0