boost::signals2
是什么?
signals2基于Boost里的另一个库signals实现了线程安全的观察者模式。在signals2中,观察者模式被称为信号/插槽(signals/slots),它是一种函数回调机制,一个信号关联了多个插槽,当信号发出时,所有关联它的插槽都会被调用。
观察者(Observer)模式中包含两种对象,分别是目标对象和观察者对象。在目标对象和观察者对象间存在着一种一对多的对应关系,当这个目标对象的状态发生变化时,所有依赖于它的观察者对象都会得到通知并执行它们各自特有的行为。
通俗地说,就好像这些观察者对象在时刻注视着目标对象(被观察)。无论何时该目标对象的状态发生变化,这些观察者对象都能够马上知道,并根据目标对象的新状态执行相应的任务。
观察者模式又叫发布-订阅(Publish-Subscribe)模式,其中的订阅表示这些观察者对象需要向目标对象进行注册,这样目标对象才知道有哪些对象在观察它。发布指的是当目标对象的状态改变时,它就向它所有的观察者对象发布状态更改的消息,以让这些观察者对象知晓。
一个目标对象的观察者对象数量是不固定的,可以随时增加新的观察者对象或取消已有的观察者对象。观察者模式的主要优点就是极大地降低了目标对象和观察者对象间的耦合,二者可以独自地改变和复用,让对系统增加功能或删除功能都很方便。
许多成熟的软件系统都用到了这种信号/插槽机制(另一个常用的名称是事件处理机制:event/event handler),它可以很好地解耦一组互相协作的类,有的语言设置直接内建了对它的支持(如c#),signals2以库的形式为c++增加了这个重要的功能。
怎么引入?
#include using namespace boost::signals2;
操作函数
signal最重要的操作函数是插槽管理connect()
函数,它把插槽连接到信号上,相当于为信号(事件)增加了一个处理的handler。
插槽可以是任意的可调用对象,包括函数指针、函数对象、以及它们的bind表达式和function对象,signal内部使用function作为容器来保存这些可调用对象。连接时可以指定组号也可以不指定组号,当信号发生时将依据组号的排序准则依次调用插槽函数。
如果连接成功connect()
将返回一个connection
,表示了信号与插槽之间的连接关系,它是一个轻量级的对象,可以处理两者间的连接,如断开、重连接、或者测试连接状态。
成员函数disconnect()
可以断开插槽与信号的连接,它有两种形式:传递组号将断开该组的所有插槽,传递一个插槽对象将仅断开该插槽。函数disconnect_all_slots()
可以一次性断开信号的所有插槽连接。
插槽的连接于使用
应用举例–观察者模式
/// /// @file signal_slot.cc /// @author lee(603933775@qq.com) /// @date 2022-11-11 14:01:32 /// // 应用观察者模式 // 举例:闹铃-护士-孩子 #include #include#include #include #include //随机一个数typedef boost::variate_generator<boost::rand48, boost::uniform_int> bool_rand;bool_rand g_rand(boost::rand48(time(0)), boost::uniform_int(0, 100));// 门铃//class Ring{public: typedef boost::signals2::signal signal_t; typedef signal_t::slot_type slot_t; boost::signals2:: connection Connect(const slot_t &s){ return alarm.connect(s); } void Press(){ std::cout<<"Ring the Alarm."<<std::endl; alarm(); }private: signal_t alarm;};// 护士//static char nurse1[128];static char nurse2[128];template class Nurse{public: Nurse(): _randNum(g_rand) {} void Action() { std::cout<<"name: "< 30){ std::cout<<"Wakeup and open door"<<std::endl; } else{ std::cout<<"is sleeping..."<<std::endl; } }private: bool_rand &_randNum;};// 孩子//static char baby1[128];static char baby2[128];template class Baby{public: Baby(): _randNum(g_rand) {} void Action(){ std::cout<<"name: "< 50){ std::cout<<"Wakeup and crying loudly"<<std::endl; } else{ std::cout<<"is sleeping in cozy..."<<std::endl; } }private: bool_rand &_randNum;};//访客//class Quest{public: void Press(Ring &ring){ std::cout<<"Come a Quest, Pressed the Ring ."<<std::endl; ring.Press(); }};int main(void){ strcpy(nurse1,"Nurse-Lucy"); strcpy(nurse2,"Nurse-Helen"); strcpy(baby1,"Baby-Tom"); strcpy(baby2,"Baby-Jerry"); Ring ring; Nurse n1; Nurse n2; Baby b1; Baby b2; Quest qst; ring.Connect(boost::bind(&Nurse::Action, n1)); ring.Connect(boost::bind(&Nurse::Action, n2)); ring.Connect(boost::bind(&Baby::Action, b1)); ring.Connect(boost::bind(&Baby::Action, b2)); qst.Press(ring); return 0;}
插槽简单调用
signal就像一个增强的function对象,使用connect()
可以使它容纳多个符合模板参数中函数签名类型的函数(插槽),形成一个插槽链表,然后在信号发生时一起调用:
/// /// @file sig_slot1.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 09:32:55 /// #include #include using std::cout;using std::endl; void slot1(){ cout<<"slot1 call"<<endl;}void slot2(){ cout<<"slot2 call"<<endl;}int main(void){ boost::signals2::signal sig; //sig.connect(&slot1); //sig.connect(&slot2); sig.connect(&slot1, boost::signals2::at_back); sig.connect(&slot2, boost::signals2::at_front); sig(); }
调用顺序和组号的控制
之前在连接插槽时省略了connect()的第二个参数connect_position,它的缺省值是at_back
,表示插槽将插入到信号插槽链表的尾部,因此上面的slot2追加到slot1后面,将在slot1之后被调用。如果令slot插到前面(at_front
)则会先调用slot2。
如果在连接时指定组号,那么每个编组都将是一个链表,其顺序规则如下:
- 各组的编号调用顺序由组合从小到大决定(除非在signal的第四个模板参数时指定)
- 每个编组的链表内部的插入顺序由at_back和at_front指定
- 未被编组的插槽如果标识是at_front,将第一个调用
- 未被编组的插槽如果标识是at_back,将最后调用
通过组号分组实现不同的组之间按照组号的大小,从小到大进行调用,同时组内的执行顺序通过at_front
、at_back
来实现。此外,不加组号的情况下使用at_front
、at_back
表示绝对的头部和尾部,而带有组号的调用在此中间完成。举例如下:
/// /// @file sig_slot1.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 09:32:55 /// #include #include using std::cout;using std::endl; templatestruct Slot{ void operator()() { cout << "Slot current N value is : " << N << endl; }}; int main(){ boost::signals2::signal sig; sig.connect(Slot(), boost::signals2::at_back); // 最后一个被调用 sig.connect(Slot(), boost::signals2::at_front); // 第一个被调用 sig.connect(5, Slot(), boost::signals2::at_back); // 组号5的最后一个 sig.connect(5, Slot(), boost::signals2::at_front);// 组号5的第一个 sig.connect(3, Slot(), boost::signals2::at_back); // 组号3的最后一个 sig.connect(3, Slot(), boost::signals2::at_front);// 组号3的第一个 sig.connect(10, Slot());// 组号10该组只有一个 sig(); return 0;}
信号的返回值
signal
如function
一样,不仅可以把输入参数转发给所以插槽,也可以传回插槽的返回值。
默认情况下signal使用合并器optional_last_value
,它将使用optional
对象返回最后被调用的插槽的返回值。
/// /// @file sig_slot1.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 09:32:55 /// #include #include using std::cout;using std::endl;templatestruct Slot{ int operator()(int x) { cout << "Slot current N * x value is : " << endl; return (N * x); }}; int main(){ boost::signals2::signal sig; sig.connect(Slot()); sig.connect(Slot()); cout << *sig(1) << endl;; cout << *sig(2) << endl;; return 0;}
signal的operator()调用这时需要传入一个整数参数,这个参数会被signal存储一个拷贝,然后转发给各个插槽。最后signal将返回插槽链表末尾slots()的计算结果,它是一个optional对象,必须用接引用操作符*来获得值(但你可以发现Slotcurrent N * x value is是输出了两次的)。
合并器
大多数时候,插槽的返回值都是有意义的,需要以某种方式处理多个插槽的返回值。
signal允许用户自定义合并器来处理插槽的返回值,把多个插槽的返回值合并为一个结果返回给用户。
/// /// @file sig_slot4.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 11:05:28 /// #include#include#include#includeusing std::cout;using std::endl;templatestruct Slot{ int operator()(int x) { cout << "Slot current N * x value is : " << endl; return (N * x); }}; templateclass combiner{public: typedef std::pair result_type; combiner(T t = T()) : v(t) {} template result_type operator()(InputIterator begin, InputIterator end) const { if (begin == end) { return result_type(); } std::vector vec(begin, end); T sum = std::accumulate(vec.begin(), vec.end(), v); T max = *max_element(vec.begin(), vec.end()); return result_type(sum, max); } private: T v;}; int main(){ boost::signals2::signal<int(int), combiner > sig; sig.connect(Slot()); sig.connect(Slot()); sig.connect(Slot()); BOOST_AUTO(x, sig(2)); cout << x.first << ", " << x.second << endl; return 0;}
combiner
类的调用操作符operator()的返回值类型可以是任意类型,完全由用户指定,不一定必须是optional或者是插槽的返回值类型。
operator()
的模板参数InputIterator是插槽链表的返回值迭代器,可以使用它来遍历所有插槽的返回值,进行所需的处理。
当信号被调用时,signal会自动把引用操作转换为插槽调用,将调用给定的合并器的operator()逐个处理插槽的返回值,并最终返回合并器operator()的结果。
如果我们不使用signal的缺省构造函数,而是在构造signal时传入一个合并器的实例,那么signal将使用逐个合并器(的拷贝)处理返回值。例如,下面的代码使用了一个有初值的合并器对象,累加值从100开始:signal<int(int),combiner > sig(combiner(100));
管理信号的连接
信号与插槽的链接并不要求是永久的,当信号调用玩插槽后,有可能需要把插槽从信号中断开,再连接到其他的信号上去。signal可以用成员函数:
- 使用
disconnect()
断开一个或一组插槽 - 使用
disconnect_all_slots()
断开所有插槽连接 - 使用函数
empty()
和num_slots()
用来检测信号当前插槽的连接状态
要断开一个插槽,插槽必须能够进行等价比较,对于函数对象来说就是重载一个等价语义的operator==
。
下面对slots
增加一个等价比较:
/// /// @file sig_slot5.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 14:40:29 /// #include #includeusing std::cout;using std::endl;using std::pair;using std::vector; templatestruct Slot{ int operator()(int x) { cout << "Slot current N * x value is : " << endl; return (N * x); }}; templatebool operator== (const Slot& a, const Slot& b){ return true;} templateclass combiner{public: typedef pair result_type; combiner(T t = T()) : v(t) { } template result_type operator()(InputIterator begin, InputIterator end) const { if (begin == end) { return result_type(); } vector vec(begin, end); T sum = accumulate(vec.begin(), vec.end(), v); T max = *max_element(vec.begin(), vec.end()); return result_type(sum, max); } private: T v;}; int main(){ boost::signals2::signal sig; // assert(sig.empty()); sig.connect(0, Slot()); sig.connect(Slot()); sig.connect(1, Slot()); assert(sig.num_slots() == 3); sig.disconnect(0); // assert(sig.num_slots() == 1); sig.disconnect(Slot()); // assert(sig.empty()); sig(2); return 0;}
更灵活的管理信号连接
signals2库提供另外一种较为灵活的连接管理方式:使用connection
对象。
每当signal使用connect()
连接插槽时,他就会返回一个connection对象。connection对象像是信号与插槽连接关系的一个句柄(handle),可以管理链接:
/// /// @file sig_slot5.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 14:40:29 /// #include #includeusing std::cout;using std::endl;using std::pair;using std::vector; templatestruct Slot{ int operator()(int x){ cout << "Slot current N * x value is : " << N*x << endl; return (N * x); }}; templatebool operator== (const Slot& a, const Slot& b){ return true;} templateclass combiner{public: typedef pair result_type; combiner(T t = T()) : v(t) {} template result_type operator()(InputIterator begin, InputIterator end) const { if (begin == end){ return result_type(); } vector vec(begin, end); T sum = accumulate(vec.begin(), vec.end(), v); T max = *max_element(vec.begin(), vec.end()); return result_type(sum, max); } private: T v;}; int main(){ boost::signals2::signal sig; // assert(sig.empty()); sig.connect(0, Slot()); sig.connect(Slot());//此连接未断开 sig.connect(1, Slot()); cout<<" sig num is : "<<sig.num_slots()<<endl; assert(sig.num_slots() == 3); sig.disconnect(0); cout<<"disconnected sig: 0, left sig num is :" <<sig.num_slots()<<endl; // assert(sig.num_slots() == 1); sig.disconnect(Slot()); cout<<"disconnected sig, left sig num is :" <<sig.num_slots()<<endl;// assert(sig.empty()); sig(2); return 0;}
另外一种连接管理对象是scoped_connection
,它是connection的种类,提供类似scoped_ptr的RAII功能:插槽与信号的连接仅在作用域内生效,当离开作用域时连接就会自动断开。
当需要临时连接信号时scoped_connection会非常有用:
/// /// @file sig_slot6.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 15:20:26 ///#include#include#include#includeusing namespace std; templatestruct Slot{ int operator()(int x) { cout << "Slot current N * x value is : " << endl; return (N * x); }}; templatebool operator== (const Slot& a, const Slot& b){ return true;} templateclass combiner{public: typedef pair result_type; combiner(T t = T()) : v(t) { } template result_type operator()(InputIterator begin, InputIterator end) const { if (begin == end) { return result_type(); } vector vec(begin, end); T sum = accumulate(vec.begin(), vec.end(), v); T max = *max_element(vec.begin(), vec.end()); return result_type(sum, max); } private: T v;}; int main(){ boost::signals2::signal sig; sig.connect(0, Slot()); assert(sig.num_slots() == 1); { boost::signals2::scoped_connection sc = sig.connect(0, Slot());//临时连接信号 assert(sig.num_slots() == 2); } assert(sig.num_slots() == 1); return 0;}
插槽与信号的连接一旦断开就不能再连接起来,connection不提供reconnet()这样的函数。但可以暂时地阻塞插槽与信号的连接,当信号发生时被阻塞的插槽将不会被调用,connection对象的blocked()
函数可以检查插槽是否被阻塞。但被阻塞的插槽并没有断开与信号的链接,在需要的时候可以随时解除阻塞。
connection对象自身没有阻塞的功能,他需要一个辅助类:shared_connection_block
,它将阻塞connection
对象,知道它被析构或者显式调用unblock()
函数:
/// /// @file sig_slot7.cc /// @author lee(603933775@qq.com) /// @date 2022-11-14 15:26:09 /// #include#include#include#includeusing namespace std;templatestruct Slot{ void operator()(int x) { cout << "Slot current N is : " << N << endl; }};templatebool operator== (const Slot& a, const Slot& b){ return true;}templateclass combiner{public: typedef pair result_type; combiner(T t = T()) : v(t) { } template result_type operator()(InputIterator begin, InputIterator end) const { if (begin == end) { return result_type(); } vector vec(begin, end); T sum = accumulate(vec.begin(), vec.end(), v); T max = *max_element(vec.begin(), vec.end()); return result_type(sum, max); }private: T v;};int main(){ boost::signals2::signal sig; boost::signals2::connection c1 = sig.connect(0, Slot()); boost::signals2::connection c2 = sig.connect(0, Slot()); assert(sig.num_slots() == 2); sig(2); cout << "begin blocking..." << endl; {//阻塞c1的连接 boost::signals2::shared_connection_block block(c1); assert(sig.num_slots() == 2); assert(c1.blocked()); sig(2); }//离开作用域可以恢复连接 cout << "end blocking.." << endl; assert(!c1.blocked()); sig(2); return 0;}