signals2
是什么?
signals2基于Boost里的另一个库signals实现了线程安全的观察者模式。在signals2中,观察者模式被称为信号/插槽(signals/slots),它是一种函数回调机制,一个信号关联了多个插槽,当信号发出时,所有关联它的插槽都会被调用。
怎么引入?
#include <boost/signals2.hpp>
using namespace boost::signals2;
类摘要
signals2库的核心是signal类,其类摘要如下:
signal的模板参数很长,总共有7个参数,这里仅列出了最重要的前4个,而且除了第一个是必须的,其他都可以用默认值:
- 第一个模板参数Signature,是一个函数类型,表示可以被signal调用的函数。比如:
signal<void(int, double)>
- 第二个模板参数Combiner,是一个函数对象,它被称为“合并器”,用来组合所有插槽的调用结果,默认是optional_last_value< R>,它使用optional库返回最后一个被调用的插槽的返回值
- 第三个模板参数是Group是插槽编组的类型,默认使用int来标记组好,也可以改为std::string等,但是一般不用改
- 第四个模板参数是GroupCompare与Group配合使用,用来确定编组的排序准则,默认是升序(std::less< Group>),因此要求Group必须定义了operator<
signal继承自signal_base,而signal_base又继承自noncopyable,因此signal是不可拷贝的。如果把signal作为自定义类的成员变量,那么自定义类也将是不可拷贝的,除非使用shared_ptr/ref来间接持有它。
操作函数
最重要的操作函数是connect(),它把插槽连接到信号上,相当于为信号(事件)增加了一个处理的handler
- 插槽可以是任意的可调用对象,包括函数指针、函数对象,以及它们的bind/lambda表达式和function对象,signal内部使用function作为容器来保存这些可调用对象。
- 连接时可以指定组号也可以不指定组号,当信号发生时将依据组号的排序准则依次调用插槽函数。
- 如果连接成功,connect()将返回一个connection对象,表示信号与插槽之间的连接关系。它是一个轻量级对象,可以处理两者间的连接,比如断开、重新连接、测试连接状态
成员函数disconnect()可以断开插槽和信号之间的连接,它有两种形式:
- 传递组号将断开该组的所有插槽
- 传递一个插槽对象仅仅断开该插槽
成员函数disconnect_all_slots()可以一次性断开信号的所有插槽连接。
当前信号所连接的插槽数量可以用num_slots()获得
成员函数empty()相当于num_slots() == 0。disconnect_all_slots()将令empty()返回true
signal提供operator(),可以接受最多9个参数。当operator()被外界调用时意味着产生了一个信号,这将导致信号所关联的所有插槽被调用。插槽调用的结果使用合并器处理后返回,默认情况下是一个optional对象。
成员函数combiner()和set_combiner()分别用于换取和设置合并器对象,通过signal的构造函数也可以在创建的时候就传入一个合并器实例。但是除非想改用其他的合并方式,通常我们直接使用默认函数创建模板参数列表中指定的合并器对象。
当signal析构时,将自动断开所有插槽连接
用法
基本用法
signal就像是一个增强的function对象,它可以容纳(使用connect()连接)多个符合模板参数中函数签名类型的函数,形成一个插槽链表,然后在信号发生时一起调用。
比如,我们有如下两个无参函数,可以用在插槽:
怎么设置用在插槽呢?
接下来就可以使用connect()来连接插槽,最后用operator()来产生信号:
在connect()时我们省略了第二个参数connect_position,其默认值是at_back,表示插槽将插入信号到尾部,因此先调用slots1,再调用slots2
当然,也可以将slost2插入头部:
使用组号
我们可以在connect()时指定插槽所在的组号。默认情况下组号是int类型,组号不一定要从0开始,它可以是任意数值,包括离散值、负值。
如果在连接时指定组号,那么每个编组都将是一个链表,其顺序规则如下:
- 各组的编号调用顺序由组合从小到大决定(除非在signal的第四个模板参数时指定)
- 每个编组的链表内部的插入顺序由at_back和at_front指定
- 未被编组的插槽如果标识是at_front,将第一个调用
- 未被编组的插槽如果标识是at_back,将最后调用
举个例子:
调用结果:
如何获取插槽的返回值
默认情况下signal使用合并器optional_last_value< R>,它将使用optional对象返回最后被调用的插槽的返回值。
举个例子:
signal的operator()调用这时需要传入一个整数参数,这个参数会被signal存储一个拷贝,然后转发给各个插槽。最后signal将返回插槽链表末尾slots< 50 >()的计算结果,它是一个optional对象,必须用解引用操作符*来获取值:
合并器
默认的合并器optional_last_value< R>并没有太多意义,它通常用在我们并不关心插槽返回值或者返回值是void的时候,但是很多时候我们需要关心多个插槽的返回值。
这时候我们可以自定义合并器来处理插槽的返回值,把多个插槽的返回值合并为一个结果返回。
合并器应该是一个函数对象(不是函数或者函数指针),具有如下形式:
combiner的operator()的返回值类型可以是任意类型,完全由用户指定,不一定必须是optional或者是插槽的返回值类型。函数的模板参数InputIterator是插槽链表的返回值迭代器,可以使用它来遍历所有插槽的返回值,进行所需的处理。
举个例子,我们自定义一个合并器,它使用pair返回所有插槽的返回值之和以及其中的最大值:
怎么用呢?我们在signal声明时需要使用第二个模板参数------合并器类型:
它相当于:
使用:
也可以在构造是传入一个合并器实例,那么signal将会处理这个合并器的拷贝处理返回值。比如下面使用了一个有初值的合并器对象:
管理信号连接
当信号调用完插槽后,可能需要将插槽从信号中断开。
要断开一个插槽,插槽必须能够等价比较,对于函数对象来说就是定义一个operator==。
举个例子:
但是这样很不方便,因为它必须知道与它连接的所有插槽的信息,还要求插槽对象必须是可以等价比较的。怎么办呢?
更灵活的管理信号连接
我们可以用connection对象来管理信号连接。