目录
一、signal类
二、基本用法
1.成员函数
2.返回值
3.使用组号
4.合并器
(1)使用signal默认构造函数
(2)实例化合并器
5.管理信号连接
(1)使用connection对象管理信号连接
(2)使用scope_connection对象管理信号连接
(3)使⽤slot类自动管理连接
三、线程安全
四、与function对比
在signals2库中,观察者模式被称为信号/插槽(signals/slots)机制,它是⼀种函数回调机制,⼀个信号关联了多个插槽,当信号发出时,所有关联它的插槽都会被调⽤。 许多成熟的软件系统都⽤到了这种信号/插槽机制(另⼀个常⽤的名称是事件处理机制:event/ event handler),它可以很好地解耦⼀组互相协作的类,有的语⾔甚⾄直接内建了对它的⽀持, signals2以库的形式为C++增加了这个重要的功能。
signals2库的核⼼是signal类,相当于C#语⾔中的event+delegate。
signal的模板参数列表相当⻓,总共有7个参数,这⾥仅列出了⽐较重要的4个,⽽且除了第⼀个 是必需的外,其他的都可以使⽤默认值。
template<
typename Signature,
typename Combiner = optional_last_value::result_type>,
typename Group = int,
typename GroupCompare = std::less,
typename SlotFunction = function,
typename ExtendedSlotFunction = typename detail::extended_signature::arity, Signature>::function_type,
typename Mutex = mutex >
signal继承⾃signal_base,⽽signal_base⼜继承⾃noncopyable,因此signal是不可拷⻉的。如果把signal作为⾃定义类的成员变量,那么⾃定义类也将是不可拷⻉的,除⾮使⽤ shared_ptr/ref来间接持有它。
connect():signal最重要的操作函数是插槽管理函数connect(),它把插槽连接到信号上,相当于为信号 (事件)增加了⼀个处理的handler。第⼆个参数connect_position,它的默认值是at_back, 表示将插槽插⼊信号插槽链表的尾部。
插槽可以是任意的可调⽤对象,包括函数指针、函数对象,以及它们的bind/lambda表达式和 function对象,signal内部使⽤function作为容器来保存这些可调⽤对象。 连接时可以指定组号也可以不指定组号,当信号发⽣时将依据组号的排序准则依次调⽤插槽函数。如果连接成功,connect()将返回⼀个connection对象,表示信号与插槽之间的连接关系,它是⼀个轻量级对象,可以处理两者间的连接,如断开、重连接或测试连接状态。
disconnect():成员函数disconnect()可以断开插槽与信号的连接,它有两种形式:传递组号将断开该组的所有插槽,传递⼀个插槽对象将仅断开该插槽。
num_slots():当前信号所连接的插槽数量可以⽤num_slots()获得。
empty():成员函数empty()相当于num_slots ()==0,但它的执⾏效率⽐num_slots()⾼。
disconnect_all_slots():函数disconnect_all_slots()可以⼀次性断开信号的所有插槽接,其结果就是令empty ()返回true。
operator():signal提供operator(),最多可以接收9个参数。当operator()被外界调⽤时意味着产⽣了⼀个信号(事件),从⽽导致信号所关联的所有插槽被调⽤。调⽤插槽的结果被合并器处理后返 回,默认情况下是⼀个optional对象。
combiner()和set_combiner():分别⽤于获取和设置合并器对象,通过signal的构造函数也可以在创建的时候就传⼊⼀个合并器的实例。但除⾮想改⽤其他的合并⽅式,通常我们可以直接使⽤默认构造函数创建模板参数列表中指定的合并器对象。
当signal析构时,将⾃动断开所有插槽连接,相当于调⽤disconnect_all_slots()。
signal就像⼀个增强的function对象,它可以容纳(使⽤成员函数connect()连接)多个符合模板参数中函数签名类型的函数(插槽),形成⼀个插槽链表,然后在信号发⽣时⼀起调⽤这些函数。
如function⼀样,signal不仅可以把输⼊参数转发给所有插槽,也可以传回插槽的返回值。默认情况下,signal使⽤合并器optional_last_value<R>,它将使⽤optional对象返回最后被调⽤的插槽的返回值。
#include
#include
#include
#include
//普通函数
int slots(int a)
{
std::cout << "slots:" << a << std::endl;
return --a;
}
//成员函数
int StudySignalClass::slots1(int a)
{
std::cout << "StudySignalClass::slots1:" << a << std::endl;
return ++a;
}
//基本用法
void StudySignalClass::Test(){
boost::signals2::signal _signal;
_signal.connect(slots);
_signal.connect(boost::bind(&StudySignalClass::slots1, this, _1)); //结合bind
_signal.connect([](int a) { //结合lambda
std::cout << "lambda表达式:" << a + 2 << std::endl;
return a + 2;
});
boost::optional num = _signal(10);
std::cout << "返回值:" << *num << std::endl;
}
connect()函数的另⼀个重载形式可以在连接时指定插槽所在的组号,默认情况下组号是int类 型。组号不⼀定要从0开始连续编号,它可以是任意的数值,离散值、负值均可。
如果在连接的时候指定组号,那么每个编组的插槽⼜是⼀个插槽链表,从⽽形成⼀个略微有些复 杂的⼆维链表,它们的顺序规则如下:
template
struct SlotsStruct {
void operator()() {
std::cout << "slot-" << N << std::endl;
}
};
void Test{
boost::signals2::signal _signal_1;
_signal_1.connect(0, SlotsStruct<0>());
_signal_1.connect(0, SlotsStruct<1>());
_signal_1.connect(1, SlotsStruct<2>());
_signal_1.connect(1, SlotsStruct<3>(), boost::signals2::at_front);
_signal_1();
}
//输出结果:
slot-0
slot-1
slot-3
slot-2
默认的合并器optional_last_value<R>并没有太多的意义,它通常⽤在我们不关⼼插槽返回值或 返回值是void的时候。但⼤多数时候,插槽的返回值都是有意义的,需要以某种⽅式处理多个插 槽的返回值。 signal允许⽤户⾃定义合并器来处理插槽的返回值,把多个插槽的返回值合并为⼀个结果返回给 ⽤户。合并器应该是⼀个函数对象(不是函数或函数指针)。
combiner的调⽤操作符operator()的返回值类型可以是任意类型,其类型完全由⽤户指定,不 ⼀定是optional或是插槽的返回值类型。函数的模板参数InputIterator是插槽链表的返回值迭代 器,可以使⽤它来遍历所有插槽的返回值,进⾏必要的处理 。
#include
//自定义合并器:使⽤pair返回所有插槽的返回值之和以及其中的最⼤值
template
class Combiner {
private:
T t;
public:
typedef std::pair result_type;
Combiner(T _t = T()) :t(_t) {
}
template
result_type operator()(InputIterator begin, InputIterator end) const {
if (begin == end)
{
int();
return result_type();
}
std::vector vec(begin, end);
//https://blog.csdn.net/zxc024000/article/details/83584878
T sum = std::accumulate(vec.begin(), vec.end(), t); //accumulate接收了三个参数,一对迭代器用来标识开始和结束区间,第三个参数0,是accumulate操作的初始值.
T max = *std::max_element(vec.begin(), vec.end());
return result_type(sum, max);
}
};
使⽤⾃定义合并器的时候,我们需要改写signal的声明,在模板参数列表中增加第⼆个模板参数 ——合并器类型。
不向构造函数传递合并器的实例,signal的构造函数会默认构造出⼀个实例。
boost::signals2::signal> _signal_combiner;
_signal_combiner.connect(SlotsStruct_2<1>());
_signal_combiner.connect(SlotsStruct_2<2>());
_signal_combiner.connect(SlotsStruct_2<3>());
auto r = _signal_combiner(10);
std::cout << "sum:" << r.first << ",max:" << r.second << std::endl; //sum:60,max:30
boost::signals2::signal> _signal_combiner(Combiner(10));
_signal_combiner.connect(SlotsStruct_2<1>());
_signal_combiner.connect(SlotsStruct_2<2>());
_signal_combiner.connect(SlotsStruct_2<3>());
auto r = _signal_combiner(10);
std::cout << "sum:" << r.first << ",max:" << r.second << std::endl; //sum:70,max:30
信号与插槽的连接并不要求是永久性的,当信号调⽤完插槽后,有可能需要把插槽从信号中断开,再将插槽连接到其他信号上去。signal可以使⽤成员函数disconnect_all_slots()断开所有插槽的连接,函数empty()和num_slots()⽤来检查信号上的当前插槽的连接状态。 ⽤成员函数disconnect()可以断开⼀个或⼀组插槽。
if (!_signal.empty())
{
std::cout << "_signal连接数:" << _signal.num_slots() << std::endl; //_signal连接数:3
}
_signal.disconnect(slots);
std::cout << "_signal连接数:" << _signal.num_slots() << std::endl; //_signal连接数:2
_signal.disconnect_all_slots();
std::cout << "_signal连接数:" << _signal.num_slots() << std::endl; //_signal连接数:0
要断开⼀个插槽,插槽必须能够进⾏等价⽐较,对于函数对象来说,这相当于重载⼀个等价语义的operator==。
template
bool operator==(const SlotsStruct&, const SlotsStruct&)
{
return true;
}
if (!_signal_1.empty())
{
std::cout << "_signal_1连接数:" << _signal_1.num_slots() << std::endl; //_signal_1连接数:4
}
_signal_1.disconnect(0);
std::cout << "_signal_1连接数:" << _signal_1.num_slots() << std::endl; //_signal_1连接数:2
_signal_1.disconnect(SlotsStruct<2>());
std::cout << "_signal_1连接数:" << _signal_1.num_slots() << std::endl; //_signal_1连接数:1
使⽤signal管理插槽有⼀点不⽅便,因为它必须知道与它连接的所有插槽的信息,还要求插槽对 象必须是可等价⽐较的,很多时候这些条件很难满⾜,⽐如有可能信号所连接的插槽是由其他库 提供的,或者插槽并不⽀持⽐较操作。
每当signal使⽤connect()连接插槽时,它就会返回⼀个connection对象。connection对象就像是信号与插槽之间连接的⼀个句柄(handle),可以管理连接。connection是可拷⻉、可赋值的,它也重载了⽐较操作符,因此它可以被安全地放⼊标准序列容器或关联容器中,成员函数disconnect()和connected()分别⽤来与信号断开连接和检测连接状态。
boost::signals2::signal _signal_3;
boost::signals2::connection cnt1 = _signal_3.connect(slots);
boost::signals2::connection cnt2 = _signal_3.connect(boost::bind(&StudySignalClass::slots1, this, _1)); //结合bind
boost::signals2::connection cnt3 = _signal_3.connect([](int a) { //结合lambda
std::cout << "lambda表达式:" << a + 2 << std::endl;
return a + 2;
});
_signal_3(10);
std::cout << "_signal_3连接数:" << _signal_3.num_slots() << std::endl; //_signal_3连接数:3
if (cnt2.connected())
{
cnt2.disconnect();
std::cout << "_signal_3连接数:" << _signal_3.num_slots() << std::endl; //_signal_3连接数:2
}
另外⼀种连接管理对象是scoped_connection,它是connection的⼦类,提供类似scoped_ptr 的RAII功能:插槽与信号的连接仅在作⽤域内⽣效,当离开作⽤域时连接就会⾃动断 开。 当需要临时连接信号时,scoped_connection会⾮常有⽤。
插槽与信号的连接⼀旦断开就不能再连接起来,connection不提供reconnet()这样的函数。
但 connection可以暂时地“阻塞”插槽与信号的连接,当信号发⽣时被阻塞的插槽将不会被调⽤, connection对象的blocked()函数可以检测插槽是否被阻塞,但被阻塞的插槽并没有与信号断开 连接,在需要的时候可以随时解除阻塞。 connection⾃身没有阻塞的功能,我们需要⽤⼀个辅助类shared_connection_block来实现阻塞。 shared_connection_block可以阻塞connection对象,直到它被析构或显式调⽤unblock()函数。
boost::signals2::signal _signal_4;
{
boost::signals2::scoped_connection scope_cnt1 = _signal_4.connect(slots);
boost::signals2::scoped_connection scope_cnt2 = _signal_4.connect(boost::bind(&StudySignalClass::slots1, this, _1)); //结合bind
boost::signals2::scoped_connection scope_cnt3 = _signal_4.connect([](int a) { //结合lambda
std::cout << "lambda表达式:" << a + 2 << std::endl;
return a + 2;
});
{
boost::signals2::shared_connection_block block_cnt(scope_cnt1);
if (scope_cnt1.blocked())
{
std::cout << "scope_cnt1连接被阻塞" << std::endl;
}
_signal_4(10);
std::cout << "_signal_4连接数:" << _signal_4.num_slots() << std::endl; //_signal_4连接数:3
}
_signal_4(10);
std::cout << "_signal_4连接数:" << _signal_4.num_slots() << std::endl; //_signal_4连接数:3
}
std::cout << "_signal_4连接数:" << _signal_4.num_slots() << std::endl; //_signal_4连接数:0
在之前讲述的信号/插槽处理系统中存在⼀个问题:如果插槽在与信号建⽴连接后被意外地销 毁了,那么调⽤信号将发⽣未定义⾏为。为了避免发⽣未定义⾏为,signals2库使⽤slot类提供了⾃动管理连接的功能,能够⾃动跟踪插槽 的⽣命周期,当插槽失效时会⾃动断开连接,以保证程序不会运⾏错误。
signals2::slot模板类可以⾃动管理插槽的连接,但通常我们不直接使⽤它,⽽是使⽤signal的 内部typedef slot_type,它已经定义好了该signal所使⽤的slot的模板参数。 如果要使⽤⾃动管理连接的功能,那么在信号连接时我们就不能直接连接插槽,⽽是要⽤slot的构造函数包装插槽,然后⽤成员函数track()来跟踪插槽使⽤的资源。(track( )函数不⽀持C++标准⾥的std::weak_ptr,所以只能使⽤boost.weak_ptr。)
slot的构造函数有⼀个有趣的地⽅,它⽀持与bind表达式相同的语法,这样它可以就地绑定函 数,没有使⽤bind表达式的构造成本。 (在使⽤bind语法时我们必须将其传递给slot原始指针,否则slot会持有⼀个shared_ptr的拷⻉,导致引⽤计数增加,妨碍shared_ptr的资源管理。)
#include
#include
void Test{
boost::signals2::signal _signal_5;
auto p = new SlotsStruct<1>;
_signal_5.connect(*p);
auto p1 = boost::make_shared>();
_signal_5.connect(boost::signals2::signal::slot_type(*p1).track(p1));
//支持与bind类似的语法
auto p2 = boost::make_shared();// boost::shared_ptr(new TestSlots());
//_signal_5.connect(boost::signals2::signal::slot_type(&TestSlots::Print, p2).track(p2)); //use_count:2
_signal_5.connect(boost::signals2::signal::slot_type(&TestSlots::Print, p2.get()).track(p2)); // use_count:1
std::cout << p2.use_count() << std::endl;
delete p;
_signal_5();
}
signal模板参数列表的最后⼀个类型参数是互斥量Mutex,默认值是signals2::mutex,它会⾃ 动检测编译器的线程⽀持程度,根据操作系统⾃动决定要使⽤的互斥量对象。通常mutex都⼯作得很好,我们不需要改变它。
signal对象在创建时会⾃动创建⼀个mutex保护内部状态,每⼀个插槽连接时也会创建⼀个新的 mutex,当信号或插槽被调⽤时mutex会⾃动锁定,因此signal可以很好地⼯作于多线程环境。 同样地,connection/shared_connection_block也是线程安全的,但⽤于⾃动连接管理的slot类不是线程安全的。
signals2库中还有⼀个dummy_mutex,它是⼀个空的mutex类,把它作为模板参数可以使 signals2变成⾮线程安全的版本,这样做是因为不使⽤锁定速度会稍微快⼀些。
signal内部使⽤function来存储可调⽤物,它的声明与function很像,它也提供了operato( )。在signal只连接了⼀个插槽的时候,它基本上与function等价。
function对象直接返回被包装函数的返回值,⽽signal则使⽤optional 对象作为返回值,signal真正的返回值需要使⽤解引⽤操作符“*”才能取得。
signal⽤于回调的灵活性⽐function强,但也使得signal的⽤法⽐较复杂,较难掌握。