- 论坛徽章:
- 0
|
该是转入到正题的时候了。前面啰啰嗦嗦扯了一大通,可能不懂的还是看不懂,有点兴趣的人却早就不耐烦了。十分抱歉,最近事情都撞到一块了,有些安排不过来。不过慢一些也好,可以一边叙述一边思考,尽量考虑的周全一些,也欢迎大家一起来出主意。
首先需要给想达到的目标定下个框框。因为有前述很多的别人早已实现好的案例作为参考,所以对于一个signal-slot大致的模样很容易清楚的,只需要模仿就可以了——这是中国特色的办法。
让我们看看使用boost.signal的一个完整的例子,这个例子基本上摘抄自前面的链接中,我把它组合到一个C/C++例子程序里,这样有条件的可以自己试着运行一下。
运行这个例子需要以下环境:boost.signal库及相关头文件,GNU C++编译器(我一般使用它来做例子测试,你也可以使用微软的VC++6.0及以后版本,但不保证测试过)。因为boost是以源代码方式发布的,需要自己编译,这个比较耗时。网络上有一些别人编译好的版本可以拿来用,你可以针对自己的环境选择。我实际使用的环境是cygwin(1)及随之发布的库及工具,国内用户可以从cygwin.cn网站安装。
这个例子十分简单,全部放到一个c++代码文件里,编译运行就可以,具体步骤就不说了。
在代码头部需要包含<boost/signals.hpp>头文件,以及<iostream>,<string>等用于测试目的,然后加上申明"using namespace std;",以符合可能的习惯。
首先申明Signal,这个申明可以作为局部或者全局变量:
boost::signal<int(float, string)> sig;
模板中的内容是一个函数类型,直接指明了signal发出时,调用接口是int(float, std::string)这样一个函数。
演示简便起见,在程序入口main函数里,我们直接发出它:
int main()
{
// ..., 之前这里我们需要预先建立signal-slot连接,后面添加
sig(3.1415926, "pi"); // 发出(emit)信号
return 0;
}
我们定义一个同样类型的自由函数供调用,方便起见,你可以把它直接放到main函数的上面:
int Func(float val, string str)
{
// 显示被调用到,我们打印调用信息
std::cout << "Func: " << "the value of " << str << " is " << val << endl;
return 0;
}
好了,可以建立signal-slot连接:
int main()
{
sig.connect(&Func); // 连接到Func函数
sig(3.1415926, "pi");
return 0;
}
可以编译实际运行一下看看效果,链接要求预先编译好的boost.signal,一般名字是libboost_signals-gcc-mt这样。
上面这个例子实际上就相当于:
int main()
{
Func(3.1415926, "pi");
return 0;
}
该没人问这样的问题吧——那干嘛还兜那么个圈子?
上面例子仅仅是相当于原始函数指针的效果,让我们继续看看调用对象成员函数怎么办。
先定义类,同样摆到main函数的上面:
class Foo {
public:
int Func(float val, string str) {
std::cout << "Foo->Func: " << "the value of " << str << " is " << val << endl;
return 0;
}
};
然后添加类的实例及对实例方法的调用,这个时候需要要代码头部添加头文件包含#include <boost/bind.hpp>
int main()
{
Foo obj; // 类Foo的实例
sig.connect(&Func); // 连接到Func函数
sig.connect(boost::bind(&Foo::Func, obj, _1, _2)); // 连接到obj的Func方法
sig(3.1415926, "pi");
return 0;
}
这里连接就复杂了一些,slot部分不再是一个简单的函数指针,而是包括成员函数指针,实例以及参数部分的一个绑定。这是生成了一个新的函数供 signal调用,把成员函数和具体实例组合在一起,所以称为函数组合或绑定;也因为相当于实际调用函数中参数的增多,被称为高阶函数,这也是来自于函数式编程领域里的标准称呼。_1,_2这样的东西被称为占位符,为了给生成的函数对象的附加参数,实际上是可以携带某种类型数据的对象。编译运行看看效果是不是和想象一样。
上述的用法有点复杂,但想法很直接,所以先举例,下面让我们看看如何调用函数对象。函数对象是一个特定的概念,该对象包含一个重载了()操作符的方法,这样调用的时候就不需要使用函数名,而可以直接以类或对象名代替,看上去就跟自由函数一样。举例如下:
struct Bar {
int operator()(float val, string str) {
std::cout << "Bar: " << "the value of " << str << " is " << val << endl;
}
};
同样把类定义放到main上面,我们增加main实体如下:
int main()
{
Foo obj; // 类Foo的实例
sig.connect(&Func); // 连接到Func函数
sig.connect(boost::bind(&Foo::Func, obj, _1, _2)); // 连接到obj的Func方法
sig.connect(Bar()); // 连接到某个Bar函数对象
sig(3.1415926, "pi");
return 0;
}
这次显得非常简洁,似乎比调用成员方法容易的多,但实际上是一回事。注意这里的Bar()本身不是调用,而只是通过缺省构造函数生成一个临时对象,然后当signal发出的时候会调用其重载的()的操作符。它等价于:
sig.connect(boost::bind(&Bar::operator(), Bar(), _1, _2)); // 这个复杂的写法是不是容易理解一些
还有这里暗含一个对象生存期的问题。无论是使用obj实例,还是临时对象,boost::bind的实现是保持一份它们的拷贝,直到实际调用产生的那一刻。临时对象因为都是一样的不用考虑,如果是实例,那么在连接建立后的对实例的修改将不会影响到slot中持有的那个对象。如果你需要的不是这样而是想要引用原有的实例对象,那么可以采用指针传递该实例,也就是:
sig.connect(boost::bind(&Foo::Func, &obj, _1, _2)); // 连接到指向obj实例指针/引用的Func方法
不要小看它们之间的差异,实际使用过程中,我们更多需要的可能是引用,而不是无数长相一致的初始对象。这种需求是如此广泛,以至于boost中有一个非常小但用处很大的库boost::ref,可以用来产生对象引用或者引用对象这个东西。它的实现如此简单但思想经典,所以我就摘抄如下:
template<class T>
class reference_wrapper
{
public:
typedef T type;
explicit reference_wrapper(T& t): t_(&t) {}
operator T& () const { return *t_; }
// ...
private:
T* t_;
};
template<class T>
inline reference_wrapper<T> const ref(T &t)
{
return reference_wrapper<T>(t);
}
然后,上面用取地址符的地方就可以换成:
sig.connect(boost::bind(&Foo::Func, boost::ref(obj), _1, _2)); // 这是不是更有c++的味道
上述的这些东西基本上等价于closuer,thunk之类的东西,这算不上什么,让我们把问题变稍微复杂些。如果有人写了个类似上述Func功能的函数Func1,但不小心参数弄反了(这是经常的事):
int Func1(string str, float val)
{
std::cout << "Func1: " << "the value of " << str << " is " << val << endl;
return 0;
}
程序可能有其他地方也用到这个函数,要改起来可能会有些混乱,没关系,我们可以这么做:
sig.connect(boost::bind(&Func1, _2, _1));
类似的,通过boost::bind我们可以在signal参数的基础上任意的增减调用参数,达到匹配最终调用的目的。
进一步,boost::bind可以通过使用函数对象作为参数,把函数和函数堆叠起来,这在函数式语言中称作currying(2),和Haskell语言一样这是为了纪念著名的逻辑学家Haskell Curry(3)。
比如,当我们获得pi值的时候,需要计算的是半径为2的圆的面积,我们分别有上述的输出函数,和一个计算面积的函数:
float Area(float r, float pi)
{
return r * r * pi;
}
然后在main代码里增加:
sig.connect(boost::bind(&Func, boost::bind(Area, 2, _1), _2));
这里看出来boost::bind返回的就是一个函数对象。
无论是上述何种做法,都局限于函数的粒度上。为此我们不得不做些预先的设计工作,或者在遇到变化的时候,随时准备添加一层类似于上述计算面积的过程。这本身当然不是什么问题,问题在于当我们说到层的时候,往往是指它们是需要独立设计和思考的。我们有限的思维能力,让我们局限于一个不算太长的代码范围内,这对像我这样天资愚笨的人而言已经是十分吃力了。而一旦出现了上述情况的话,我们就不得不打断当前的工作,跳转到其他一个代码空间里,做出某种变更,然后再返回到刚才被打断的地方。这分散了我们的注意力,破坏了已经存在于大脑中一个连续的逻辑思维过程,降低了效率和提高了出错概率。
这个时候我们需要的是一个可以不用跳来跳去就可以完成工作的办法,这就是lambda表达式所能带来的好处。
什么是lambda表达式?数学意义上的定义就不在这里讨论了(我水平也不够)。程序设计语言中的lambda表达式也就是指由一系列运算符结合而成的表达式,它自身也可以同时成为一个新的表达式的部分。在命令式语言里,你可以把一个语句块看成就是一个lambda表达式,虽然它不是严格意义上的一个表达式。和函数式语言中不同的是,在C语言里,你不能把语句块直接拿来作为函数使用,C++也同样如此。但是在Object C里有block的概念(4),可以把语句块当作可以调用的对象,在一些动态语言里也有类似的东西。我们用lambda表示式重写上面的例子:
sig.connect([](float pi, string str) { Func(2 * 2 * pi, str); });
connect的参数就是一个lambda表达式,它和函数很像,只是没有名字并且和函数体语句块一起被定义。lambda表达式可以组合成小到一个语句的任意粒度,如果我们的调用对象粒度不大,并且不需要重用的话,这种表述方式无疑大大提高了编码效率。
上述的例子是按照已经被接纳成为C++ 0x一部分的lambda表达式标准提议(5)写就,相信不久将来就会出现大众的代码里。
关于lambda表达式,boost里面也早已有实现,而且有两个,一个是boost::lambda,还有一个是作为boost::spirit一部分的boost::phoenix(6)。两者大同小异,前者比较早,已经没有什么变化,后者更新一些,支持更加全面一些。我们看看 boost::lambda如何来写上面的例子:
sig.connect(boost::lambda::bind(&Func, boost::lambda::_1 * 2 * 2, boost::lambda::_2));
去掉boost::lambda这个长长的前导命名空间,是不是就很漂亮了。总体上boost::lambda在表达一些简单的运算是足可以胜任的,但在表示复杂的代码流程和引用复杂的成员对象时有些繁琐,因此使用上会受到一定的制约。
注:后面的部分例子只是为了展示一些高阶能力,细节上没有面面俱到,感兴趣的可以自己去尝试。
到目前为止我们演示了signal-slot主要部分:连接(connect)和发出(emit),使用中还有对连接的管理,比如至少可以断开;以及对返回值的使用。这个例子中signal的函数原型具有int类型的返回值,可以在发出signal的地方获得,也就是把signal当作函数使用。在C++ 中这很正常,因为它实际上就是一个函数对象。不过这些都不关键,暂时就不一一讨论了。
(1)http://cygwin.com
(2)http://en.wikipedia.org/wiki/Currying
(3)http://en.wikipedia.org/wiki/Haskell_Curry
(4)http://en.wikipedia.org/wiki/Blocks_%28C_language_extension%29
(5)http://www.open-std.org/JTC1/SC22/WG...2009/n2927.pdf
(6)http://spirit.sourceforge.net/dl_doc...tml/index.html
[ 本帖最后由 TiGEr.zZ 于 2009-10-14 21:14 编辑 ] |
|