1.概述
本文中Boost.Signals2库提供了一个简单的方法在C++中应用这一模式。严格来说,Boost.Function能够将一个以上的事件处理器关联至单个事件。因此,Boost.Signals可以更好地支持事件驱动的开发,当需要进行事件处理时,应作为第一选择。
2.信号signals
Boost.Signals所实现的模式被命名为'信号至插槽'(signal to slot),它基于以下概念:当对应的信号被发出时,相关联的插槽即被执行。原则上,你可以把单词'信号'和'插槽'分别替换为'事件'和'事件处理器'。不过,由于信号可以在任意给定的时间发出,所以这一概念放弃了'事件'的名字。
因此,Boost.Signals没有提供任何类似于'事件'的类。相反,它提供了一个名为boost::signal的类,定义与boost/signal.hpp,实际上,这个头文件是唯一一个需要知道的,因为它会包含其它相关的头文件。
eg1:
#include
void func()
{
cout << "hello ,boost c++." << endl;
}
int main()
{
boost::signals2::signal s;
s.connect(func);
s();
return 0;
}
运行结果:
hello ,boost c++
boost::signal实际上被实现为一个模板函数,具有被用作事件处理器的函数的签名,该签名也是它的模板参数。在这个例子中,只有签名为void()的函数可以被成功关联至信号s。
函数func()被通过connect()方法关联至信号s,由于func()符合要求的void()签名,所以该关联成功建立。因此当信号s被触发时,func()将被调用。
信号是通过调用s来触发的,就像普通的函数那样。这个函数的签名对应于作为模板参数掺入的签名:因为void()不要求任何参数,所以括号内是空的。
调用s会引发一个触发器,进而执行相应的func()函数-之前用connect()关联了的。
同一个例子也可以用Boost.function来实现。
eg2:
#include
int main()
{
boost::function f;
f = func;
f();
return 0;
}
运行结果:
hello ,boost c++.
和前一个例子相类似,func()被关联至f,当f被调用时,就会相应执行func()。boost.function仅限于这种情况下适用,而Boost.Signls则提供了多得多的方式,如关联多个函数至单个特定信号。
eg3:
#include
#include
using namespace std;
void func()
{
cout << "hello ,boost c++." << endl;
}
void func2()
{
cout << "hello,world!" << endl;
}
int main()
{
boost::signals2::signal s;
s.connect(func);
s.connect(func2);
s();
return 0;
}
运行结果:
hello ,boost c++.
hello,world!
boost::signal可以通过反复调用connect()方法来把多个函数赋值给单个特定信号。当该信号被触发时,这些函数被按照之前用connect()进行关联的顺序来执行。
另外,执行的顺序也可以通过另一个重载版本来明确指定,该重载版本要求以一个int类型的值作为额外的参数。
eg4:
void func1()
{
std::cout << "Hello" << std::flush;
}
void func2()
{
std::cout << ", world!" << std::endl;
}
int main()
{
boost::signals2::signal s;
s.connect(1, func2);
s.connect(0, func1);
s();
}
运行结果:
Hello,world!
和之前一个例子一样,func1()在func2()之前执行。要释放某个函数与给定信号的关联,可以使用disconnect()方法。此外,boost::signal还提供了几个方法:
num_slots()返回已关联函数的数量,如果没有函数被关联,则返回0。这这种特定情况下,可以用empty()方法来代替。
disconnect_all_slots()方法所做的实际上正是它的名字所表达的:释放所有已有的关联。
看完了函数如何被关联至信号,以及弄明白了信号被触发时的调用关系后,还有一个问题就是:这个函数的返回值去哪里?
eg5:
#include
#include
int func1()
{
return 1;
}
int func2()
{
return 2;
}
int main()
{
boost::signals2::signal s;
s.connect(func1);
s.connect(func2);
cout << s() << endl;
}
运行结果:
2
func1()和func2()都具有int类型的返回值。s将处理两个返回值,并将它们都写出至标准输出流。那么到底会发生什么呢?
以上例子实际上会把2写出至标准输出流。两个返回值都会被s正确接收,但除了最后一个值,其他值都会被忽略。缺省情况下,所有关联函数中,实际上只有最后一个返回值被返回。
我们可以定制一个信号,令每个返回值都被相应的处理。为此,要把一个称为合成器(combiner)的东西作为第二个参数传递给boost::signal。
eg6:
#include
#include
#include
int func1()
{
return 1;
}
int func2()
{
return 2;
}
namespace test {
template
struct min_element
{
typedef T result_type;
template
T operator()(InputIterator first, InputIterator last)const
{
return T(first, last);
}
};
}
int main()
{
boost::signals2::signal > > s; //此处使用了test命名空间,否则vs2019会报min_elemet符号不明确
s.connect(func1);
s.connect(func2);
std::vector a = s();
cout << *std::min_element(a.begin(),a.end()) << endl;
}
运行结果:
1
合成器是一个重载了operator()()操作符的类。这个操作符会被自动调用,传入两个迭代器,指向某个特定信号的所有返回值。以上例子使用了标准c++算法std::min_element()来确定并返回最小的值。
如果直接调用s(),并用cout打印,不幸的是,我们不可能把象std::min_element()这样的一个算法直接传递给boost::signal作为一个模板参数。boost::signal要求这个合成器定义一个名为result_type的类型,用于说明operator()()操作符返回值的类型。由于在标准c++算法中缺少这个类型,所以在编译时会产生一个相应的错误。
上述例子把所有的s()的返回值报错在一个vector中,然后再调用min_element算法得到结果。
3.连接Connections
可以借助提供的connect()和disconnect()成员功能来管理功能boost::signals2::signal。因为connect()返回类型为Object的对象Boost::signals2::connection,所以也可以以不同的方式管理关联。
eg7:
#include
#include
int main()
{
boost::signals2::signal s;
boost::signals2::connection c = s.connect(
[] {
std::cout << "Hello, world!" << std::endl;
}
);
s();
c.disconnect();
}
运行结果:
Hello,world!
上述例子中,boost :: signals2 :: signal的disconnect()成员函数需要传递函数指针。然而通过在boost ::signals2 :: connection对象上调用disconnect()可以避免这种情况。
boost::signals2::shared_connection_block使用时可以短时间内阻止功能而不从信号中删除关联。
eg8:
#include
int main()
{
boost::signals2::signal s;
boost::signals2::connection c = s.connect([] { std::cout <
s();
boost::signals2::shared_connection_block b{c};
s();
b.unblock();
s();
}
运行结果:
Hello,world!
Hello,world!
其实执行了两次lambda函数。信号s触发了3次,但是由于boost::signals::shared_connection_block创建了一个对象类型来阻止调用,因此第二次不调用lambda函数。一旦对象超出范围,该块将被自动删除。也可以通过调用明确删除一个块unblock()。因为它是最后一个触发器之前调用的,所以再次执行了对lambda函数的最终调用。
另外unblock(),boost::signals2::shared_connection_block提供了成员函数block()和blocking()。前者用于在调用之后阻止连接unblock(),而后者则可以检查当前是否阻止了连接。
请注意,boost::signals2::shared_connection_block带有单词“ shared ”是有原因的:boost::signals2::shared_connection_block可以使用同一连接初始化多个类型的对象。
eg9:
using namespace boost::signals2;
int main()
{
signal s;
connection c = s.connect([] { std::cout << "Hello,world!\n"; });
shared_connection_block b1{ c, false };
{
shared_connection_block b2{ c };
std::cout << std::boolalpha << b1.blocking() << '\n';
s();
}
s();
}
运行结果:
false
Hello, world!
两次访问s,但是仅第二次调用lambda函数。程序Hello, world!仅一次写入标准输出流。因为false作为第二个参数传递给构造函数,所以类型的第一个对象boost::signals2::shared_connection_block不会阻塞与信号s的连接。因此,调用blocking()对象b1返回false。
但是,在第一次访问s时不会执行lambda函数,因为访问仅在boost::signals2::shared_connection_block实例化第二个类型的对象之后发生。通过不向构造函数传递第二个参数,该连接将被对象阻止。当第二次访问时,执行lambda函数,因为一旦块被自动移除b2去的范围进行。
一旦其成员函数与信号相关联的对象被破坏,Boost.Signals2就可以释放连接。
除了boost::signals2::connection这个外,还有一个名为boost::signals::scoped_connection的类,它会在析构时候自动释放连接。
eg10:
void func()
{
cout << "func id called." << endl;
}
int main()
{
boost::signals2::signal s;
{
boost::signals2::scoped_connection c =s.connect(func);
}
s();
cout<
}
运行结果:
Love China!
因为连接对象c在信号触发之前被销毁,所以func()不会被调用。
boost::signals::scoped_connect实际上是派生自 boost::signals::connection 的,所以它提供了相同的方法。它们之间的区别仅在于,在析构 boost::signals::scoped_connection 时,连接会自动释放。
虽然 boost::signals::scoped_connection 的确令自动释放连接更为容易,但是该类型的对象仍需要管理。如果在其它情形下连接也可以被自动释放,而且不需要管理这些对象的话,就更好了。
eg11:
#include
#include
#include
#include
class test
{
public:
voidfun()const
{
std::cout<< "test" << std::endl;
}
private:
};
int main()
{
boost::signals2::signal s;
{
std::auto_ptrw(new test());
s.connect(boost::bind(&test::fun,w.get()));
}
cout<< s.num_slots() << endl;
s();
}
运行结果:
1
test
以上程序使用 Boost.Bind 将一个对象的方法关联至一个信号。在信号触发之前,这个对象就被销毁了,这会产生问题,存在一定的风险。我们不传递实际的对象 w,而只传递一个指针给 boost::bind()。在 s() 被实际调用的时候,该指针所引向的对象已不再存在。
可以如下修改这个程序,使得一旦对象 w 被销毁,连接就会自动释放。仅需的修改是让 test 类继 boost::signals2::trackable。
如果现在再执行,num_slots() 会返回 0 以确保不会试图调用已销毁对象之上的方法。 当使用对象的指针而不是对象的副本来关联函数至信号时,boost::signals2::trackable 可以显著简化连接的管理。
当继承了 boost::signals2::trackable后运行结果为:
0
从而提高了程序的安全性。
总结:
参考:http://zh.highscore.de/cpp/boost/eventhandling.html
https://theboostcpplibraries.com/boost.signals2-connections