1、signals2库
signals2库实现了线程安全的观察者模式,在signals2中观察者模式被称为信号/插槽(signals/slots),它是一种函数回调机制。一个信号可以关联一个或多个插槽,当信号发出时,所有关联它的插槽都会被调用。
signals2位于名字空间boost::signals2,使用需要包含头文件"boost/signals2.hpp",而且在VC下编译signals2时,应该在stdafx.h中加入预声明#define _SCL_SECURE_NO_WARNINGS,或者在项目属性->C/C++->预处理器->预处理定义中添加_SCL_SECURE_NO_WARNINGS。
2、signal
在signals2中使用signal类来表示信号,一个信号可以关联一个或多个插槽,插槽可以是函数指针、函数对象、bind表达式、function对象。调用signals的成员函数connect()来将插槽连接到信号上,对signal对象调用()即为产生一个信号(signal提供了operator()),从而导致连接的所有插槽被调用。可以看到signal的功能以及使用方式跟function很像,可以把function看做是只有一个插槽的signal。
产生信号即调用signal的operator()的返回值是一个optional类型的对象,可以使用解引用操作符*来获得真正的返回值,而且默认情况下,这个返回值是最后被调用的插槽的返回值。signal会把参数传递给所有连接的插槽。
就像function一样,signal也是一个模板类,第一个模板参数是插槽函数类型签名,除了第一个模板参数外其他的模板参数都有缺省值,所以可以只传入第一个参数。
signal::connect()的第一个参数为一个插槽,第二个参数为插入的位置,插入位置决定了插槽的调用顺序,默认值为at_back往后插入,at_front为往前插入,返回值是一个connection对象,可以这个对象来管理连接,如断开连接(disconnect)、测试连接(connected)等。
其它成员函数:
num_slots()用来获得连接的插槽的个数,empty()用来判断连接的插槽数量是否为0。
combiner()用来获取合并器,set_combiner()用来设置合并器,不过一般是在signal构造函数中直接设置合并器。
disconnect()用来断开连接,disconnect_all_slots()用来断开所有连接的插槽,signal对象析构时会自动调用disconnect_all_slots()。
使用示例:
#include "boost/signals2.hpp" int slots1(int n) { int iRet = n * n; cout << "slot1 called, return value: " << iRet << endl; return iRet; } int slots2(int n, int x) { int iRet = n * n * x; cout << "slot2 called, return value: " << iRet << endl; return iRet; } int slots3(int n) { int iRet = n * n; cout << "slot3 called, return value: " << iRet << endl; return iRet; } int slots4(int n) { int iRet = n * n; cout << "slot4 called, return value: " << iRet << endl; return iRet; } int main() { boost::signals2::signal<int(int)> sig; boost::signals2::connection con1 = sig.connect(slots1); boost::signals2::connection con2 = sig.connect(bind(slots2, _1, 100)); boost::signals2::connection con3 = sig.connect(slots3, boost::signals2::at_front); boost::signals2::connection con4 = sig.connect(slots4); bool b = con4.connected(); con4.disconnect(); b = con4.connected(); int iRet = *sig(1); cout << "return value: " << iRet << endl; return 0; }
程序输出为:
3 、signal的模板参数
前边我们说过,signal是一个模板类,它的第一个模板参数是插槽函数类型签名,其余模板参数都有缺省值。
①、signal<>的第二个模板参数
默认情况下,产生信号即调用signal对象的operator()的返回值是最后被调用的插槽的返回值,eg:
#include "boost/signals2.hpp" template<int N> class MySlots //模板函数对象类,可以用来生成一系列的插槽 { public: int operator()(int x) { cout << "slot" << N << " called" << endl; return x * N; } }; int main() { boost::signals2::signal<int(int)> sig; boost::signals2::connection con1 = sig.connect(MySlots<1>()); boost::signals2::connection con2 = sig.connect(MySlots<2>()); boost::signals2::connection con4 = sig.connect(MySlots<3>()); int iRet = *sig(10); //iRet为30 return 0; }
signal<>的第二个模板参数被称为“合并器”,它是一个函数对象,可以用来自定义signal()的返回结果,即调用signal的operator()产生信号后返回值。上面说过,缺省的合并器会返回一个optional类型的对象,它是最后被调用的插槽的返回值,我们可以通过自定义合并器来将多个插槽的返回值合并为一个结果以返回给用户。合并器的两个模板参数是迭代器类型,利用他们可以遍历所有插槽的返回值,合并器的返回值由合并器对象的operator()返回值来确定。以下示例代码生成了一个自定义的合并器,并返回一个pair类型,first为所有插槽返回值之和,second为所有插槽返回值中的最大值:
#include "boost/signals2.hpp" template<int N> class MySlots //模板函数对象类,可以用来生成一系列的插槽 { public: int operator()(int x) { cout << "slot" << N << " called" << endl; return x * N; } }; template<typename T> class CCombiner //模板函数对象类 { public: typedef pair<T, T> result_type; template<typename Iterator> result_type operator()(Iterator begin, Iterator end)const { if (begin == end) return result_type(); vector<T> vec(begin, end); T sum = accumulate(vec.begin(), vec.end(), 0); T max = *max_element(vec.begin(), vec.end()); return result_type(sum, max); } }; int main() { boost::signals2::signal<int(int), CCombiner<int>> sig; //在这里第二个模板参数我们传入CCombiner<int>类型即可,因为signal的构造函数里会自动生成一个CCombiner<int>实例 boost::signals2::connection con1 = sig.connect(MySlots<1>()); boost::signals2::connection con2 = sig.connect(MySlots<2>()); boost::signals2::connection con4 = sig.connect(MySlots<3>()); auto ret = sig(10); int sum = ret.first; //60 int max = ret.second; //30 return 0; }
②、signal<>的第三个模板参数
signal<>的第三个模板参数是“插槽编组”的类型,默认为int类型,signal的connect()函数有一个可以指定插槽所属编组的重载函数,可以通过它们来设置插槽函数的调用顺序:各编组的调用顺序默认由组号从小到大决定,可以通过signal<>的第四个模板参数来改变排序方式。connect()时未被编组的插槽如果位置标志是at_front则在所有插槽调用之前调用,at_back为在所有插槽调用之后调用。
例如以下代码,插槽的调用顺序为:slots5, slots1, slots6, slots3, slots2, slots4
boost::signals2::signal<void(int)> sig; boost::signals2::connection con1 = sig.connect(1, slots1); boost::signals2::connection con1 = sig.connect(1, slots6); boost::signals2::connection con2 = sig.connect(3, slots2); boost::signals2::connection con3 = sig.connect(2, slots3); boost::signals2::connection con4 = sig.connect(slots4); boost::signals2::connection con5 = sig.connect(slots5, boost::signals2::at_front); sig(100);
③、signal<>的第四个模板参数
signal<>的第四个模板参数用来指定编组的排序规则,默认是升序,因此要求signal<>的第三个模板参数即插槽编组的类型必须支持<或定义了operator<。
4、管理信号与插槽的连接
我们一般使用signal::connect()返回的connection对象来管理信号的连接,比如使用connection::disconnect()来断开连接,使用connection::connected()来检测是否连接,使用connection::blocked()函数来检测插槽是否被阻塞。connection是可拷贝可赋值的,它也重载了 比较操作符,所以可以直接作为序列容器或关联容器的元素。
还可以使用connection的子类scoped_connection对象来管理连接,它的额外作用是当scoped_connection对象离开作用域的时候自动断开连接。
shared_connection_block对象提供了阻塞插槽和信号的连接的方法block(),我们一般将一个connection对象赋给一个shared_connection_block对象,从而使这个shared_connection_block对象拥有对connection的管理权。解除阻塞使用unblock(),当shared_connection_block对象离开作用域的时候会自动调用unblock()来解除阻塞。
#include "boost/signals2.hpp"
template<int N>
class MySlots //模板函数对象类,可以用来生成一系列的插槽
{
public:
int operator()(int x)
{
cout << "slot" << N << " called" << endl;
return x * N;
}
};
int main()
{
boost::signals2::signal<int(int)> sig;
boost::signals2::connection con1 = sig.connect(MySlots<1>());
boost::signals2::connection con2 = sig.connect(MySlots<2>());
boost::signals2::connection con4 = sig.connect(MySlots<3>());
con1.disconnect(); //con1断开连接
assert(!con1.connected());
assert(sig.num_slots() == 2);
{
boost::signals2::scoped_connection con5 = sig.connect(MySlots<4>());
assert(sig.num_slots() == 3);
}
assert(sig.num_slots() == 2); //con5离开作用域,自动断开连接
{
boost::signals2::shared_connection_block cb(con4);
cb.block(); //con4被阻塞,将不会被调用
}
assert(!con4.blocked()); //cb离开作用域,con4自动解除阻塞
return 0;
}
5、插槽销毁的处理
如果插槽在与信号建立连接后又被销毁了,那么信号调用将发生未定义的行为。为了防止意外发生,在连接信号的时候我们不直接使用插槽,而是使用signal<>::slot_type()函数包装插槽,并使用成员函数track()来跟踪插槽,这样当插槽销毁的时候会自动断开连接,eg:
#include "boost/smart_ptr.hpp" #include "boost/signals2.hpp" template<int N> class MySlots //模板函数对象类,可以用来生成一系列的插槽 { public: int operator()(int x) { cout << "slot" << N << " called" << endl; return x * N; } }; int main() { typedef boost::signals2::signal<int(int)> signal_t; typedef signal_t::slot_type slot_t; signal_t sig; boost::shared_ptr<MySlots<20>> sp(new MySlots<20>); sig.connect(slot_t(ref(*sp)).track(sp)); sp.reset(); //插槽被销毁,自动关闭连接 assert(sig.num_slots() == 0); sig(10); //没有插槽会被调用 return 0; }
因为插槽可以是函数指针、函数对象、bind表达式、function对象,所以signal<>::slot_type()也支持函数指针、函数对象、bind表达式、function对象。signal<>::slot_type()的另一个特色是支持bind表达式相同的语法,这样就可以就地绑定函数,避免使用bind表达式的构造成本。eg:
#include "boost/smart_ptr.hpp"
#include "boost/function.hpp"
#include "boost/signals2.hpp"
template<int N>
class MySlots //模板函数对象类,可以用来生成一系列的插槽
{
public:
int operator()(int x)
{
cout << "slot" << N << " called" << endl;
return x * N;
}
};
class MySlots2
{
public:
int operator()(int x, int y)
{
cout << x << ", " << y << endl;
return y;
}
typedef int result_type;
};
int main()
{
typedef boost::signals2::signal<int(int)> signal_t;
typedef signal_t::slot_type slot_t;
signal_t sig;
boost::shared_ptr<MySlots<10>> sp1(new MySlots<10>);
boost::function<int(int)> func = ref(*sp1);
sig.connect(slot_t(func).track(sp1));
boost::shared_ptr<MySlots2> sp2(new MySlots2);
sig.connect(slot_t(MySlots2(), 20, _1).track(sp2));
sp1.reset(); //插槽被销毁,自动关闭连接
assert(sig.num_slots() == 1);
sig(10); //只有MySlots2插槽会被调用
return 0;
}
6、让signal支持拷贝
signal是noncopyable的子类,所以它不能被拷贝或赋值,如果我们的类中含有signal成员变量,那么类的对象也不能被拷贝、赋值。如果确实需要拷贝含有signal数据成员的类的对象的话,可以保存signal的shared_ptr作为成员变量,这样多个类的对象之间可以通过这个shared_ptr来共享signal对象。eg:
#include "boost/smart_ptr.hpp" #include "boost/signals2.hpp" class CDemoClass { public: typedef boost::signals2::signal<void()> signal_t; typedef boost::shared_ptr<signal_t> shared_ptr_t; CDemoClass():m_spSig(new signal_t){} //构造函数 public: shared_ptr_t m_spSig; //成员变量 }; void print() { cout << "print() called" << endl; } class CTest { public: CTest() { cout << 1 << endl; } CTest(const CTest& t) { cout << 2 << endl; } }; int main() { CDemoClass dc1; int n1 = dc1.m_spSig.use_count(); //1 CDemoClass dc2(dc1); int n2 = dc2.m_spSig.use_count(); //2 dc1.m_spSig->connect(print); (*(dc2.m_spSig))(); return 0; }
7、插槽的调度
“合并器”也可以用来设置当一个插槽的返回值满足特定条件后就终止其它插槽的调用,eg:
#include "boost/signals2.hpp" int slots1(int n) { int iRet = n * n; cout << "slot1 called, return value: " << iRet << endl; return iRet; } int slots2(int n) { int iRet = n; cout << "slot2 called, value: " << iRet << endl; return iRet; } class CCombiner { public: typedef bool result_type; template<typename InputIterator> result_type operator()(InputIterator begin, InputIterator end)const { while (begin != end) { if (*begin > 100) return true; } return false; } }; int main() { boost::signals2::signal<int(int), CCombiner> sig; sig.connect(slots1); sig.connect(slots2); sig(20);//slots2不会被调用 return 0; }
8、让插槽来管理连接
如果向让插槽来管理连接,那么可以给插槽函数传递当前connection对象来解决。为了完成这个功能,插槽的声明形式需要改变一下,使其能够接受connection对象。同时,在连接插槽的时候是调用signal::connect_extended(),而且必须使用extended_slot_type的类bind语法。eg:
#include "boost/signals2.hpp" template<int N> class CSlots { public: typedef boost::signals2::connection connection_t; int operator()(const connection_t& con, int x) { cout << "connection state: " << con.connected() << endl; return x * N; } typedef int result_type; }; int main() { typedef boost::signals2::signal<int(int)> signal_t; typedef signal_t::extended_slot_type ex_slot_t; signal_t sig; sig.connect_extended(ex_slot_t(CSlots<100>(), _1, _2)); int iRet = *sig(5); return 0; }
9、线程安全
signal可以很好的工作于多线程环境,它是线程安全的。同样地,connection和shared_connection_block也是线程安全的,但signal<>::slot_type()函数构造的对象不是线程安全的。
10、signal与function
当signal只连接了一个插槽的时候基本上可以与function替换。但需要注意二者的返回值不同,signal调用函数的时候使用optional对象作为返回值,真正的返回值需要使用解引用操作符*才能取得。function调用函数的时候直接返回被包装函数的返回值。