社区应用 最新帖子 精华区 社区服务 会员列表 统计排行 银行

  • 1033阅读
  • 3回复

boost::lambda库介绍

级别: 管理员
发帖
6294
金币
2497
威望
2887
贡献值
0
元宝
0
当然这篇不是讲boost::lambda的用法的,如有有人感兴趣,可以参照:http://www.boost.org/doc/libs/1_38_0/doc/html/lambda.html


boost::lambda很复杂,一两句话也说不清楚,我尽力描述的简单一点,慢慢增加难度。


对我来说,boost::lambda省了不少事,我是喜欢stl algorithm的,现在基本上除非特殊情况,程序里面都不出现循环了。用for_each, transform 等等都可以搞定,否则,就要考虑数据结构和算法是不是有问题了。


但是问题在于每次用for_each的时候,都要定义一个仿函数(Modern C++ Design 这么叫的),麻烦,程序看起来也不怎么优雅顺畅,总要停下来去看那个仿函数到底干什么了,写的时候还要想是不是要泛化,很头痛。有了boost::lambda,就爽多了。看起来一目了然,写起来简单明了,不用关心类型。(是不是搞推销的?)


言归正传吧。


比如下面这段代码:


1 vector<int> v;
2 v.push_back(12);
3 v.push_back(1342);
4 v.push_back(23);
5
6 struct OP
7 {
8     void operator()(int &i)
9     {
10         i = 3;
11     }
12 };
13
14 for_each(v.begin(), v.end(), OP());


够简单吧,把整个容器的值都改成3. 看到那个OP了吧,很简单的一件事情,非要让人写这么个struct,要是写成class,还要public,更郁闷。


看看用了boost::lambda以后的效果吧。
1 vector<int> v;
2 v.push_back(12);
3 v.push_back(1342);
4 v.push_back(23);
5
6 for_each(v.begin(), v.end(), _1 = 3);
7


比不用lambda整整少了一个OP定义呀,因为 string("OP()").length() == string("_1=3").length() .


那么,怎么才能达到这个效果呢,在这里,先假设我们只用int类型,关于泛化,下一篇再说,一次吃多了消化不良的。


熟悉for_each的都知道,for_each的第3个参数是个函数对象(我不用指针抱歉),注意区分仿函数和函数对象,仿函数是个类型,函数对象是个对象。


那么也就是说  _1 = 3 的结果应该是个函数对象,而且是个一元函数对象(不了解的去看for_each实现)。知道了这个,很容易写个大概:


1 struct op
2 {
3     ??? operator()(int& i)
4     {
5         i = ???;
6     }
7 };
8
9 struct place_holder
10 {
11     op operator=(int i)
12     {
13         return op???;
14     }
15 };
16
17 place_holder _1;
18
19 for_each(v.begin(), v.end(), _1 = 3);


???不是乱码,这里只是暂时不知道写什么。
我说过,这篇里面类型都是int,但是那个3怎么处理呢,明显要保存的仿函数里面去么,所以上面的代码进一步修改,
个OP里面增加变量,来保存3,函数返回值现在不重要,就写成int吧,以后有问题再说。
于是代码变成:


1 struct op
2 {
3     op(int i)
4         : _i(i)
5     {}
6
7     int _i;
8
9     int operator()(int& i)
10     {
11         i = _i;
12     }
13 };
14
15 struct place_holder
16 {
17     op operator=(int i)
18     {
19         return op(i)
20     }
21 };
22
23 place_holder _1;
24
25 for_each(v.begin(), v.end(), _1 = 3);


问题解决。


看到这里,整个程序已经可以执行了。把容器的值改成3,没问题吧。


整片文章都在一个假设之下,就是只用int,那要是不用int呢,情况就复杂一点了,下篇再讨论。 如果熟悉template的话,下篇很容易,否则,复习咯。

相关话题

关键词: boost c++ 编程
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638 欢迎加入本论坛超级QQ群:122538123
级别: 管理员
发帖
6294
金币
2497
威望
2887
贡献值
0
元宝
0
只看该作者 沙发  发表于: 01-19
上次说到在假设类型int下,成功的实现了一个“lambda”,这次,当然不能还在int的假设下了。我们的武器就是模板,说起来模板,话就长了。
这里略过,讲重点。


这是上次最后的代码,为了方便描述,再贴一份。


1 struct op
2 {
3     op(int i)
4         : _i(i)
5     {}
6
7     int _i;
8
9     int operator()(int& i)
10     {
11         i = _i;
12     }
13 };
14
15 struct place_holder
16 {
17     op operator=(int i)
18     {
19         return op(i)
20     }
21 };
22
23 place_holder _1;
24
25 for_each(v.begin(), v.end(), _1 = 3);


要去掉对int的依赖,先仔细想想对int的依赖都在哪里?说明白点就是整个程序,哪里都有int?
1.  op的构造函数参数是int
2.  op里面的成员变量 _i的类型是int
3.  op的operator() 的返回值和参数都有int
4.  place_holder的operator=的参数是int


当然 vector<int> 也有int,但这个不算 :)


总的来说,int和 一个变量 int op::_i, 三个函数 op::op(int i), op::operaator(int& i) 和 place_holder::operator=(int) 有关系,这一点很重要,类和函数在泛型中的作用不一样,
看看 http://www.cppblog.com/yindf/archive/2009/02/20/74397.html 中说的类模板和函数模板的区别吧。


再细分一点,和 _1 有关的int就只有op::operator()一个,其他都和 _1 没关系。
剩下的都和 3 有关系,想想 3 的传递路径, 从 place_holder::operator = 到 op::op(int i), 再到 op::_i。


也就是说op::operator()要一个独立的模板参数。
想想看,其实op::op(int i) 和 op::_i 是一个东西,构造函数就是为了初始化这个变量。所以这里选择泛化整个op,就是说构造函数的参数和变量是同一个类型。
对于place_holder::operator =, 是要泛化整个place_holder呢,还是只泛化place_holder::operator=呢,当然泛化函数,因为类不会进行类型推导。
意思是如果泛化类的话,你就要有为无数类型特化过的place_holder,这里很难理解,不理解的话,继续看下去吧。


现在就开始实做吧。




1 template <typename _U>
2 struct op
3 {
4     op(_U i)
5         : _i(i)
6     {}
7
8     _U _i;
9
10     template<typename _T>
11     _T& operator()(_T& i)
12     {
13         i = _i;
14     }
15 };
16
17 struct place_holder
18 {
19     template <typename _T>
20     op<_T> operator=(_T i)
21     {
22         return op<_T>(i)
23     }
24 };
25
26 place_holder _1;
27
28 for_each(v.begin(), v.end(), _1 = 3);


好了,泛化完成。难道就这么简单?事实就是这么简单。
来分析一下模板推导的过程吧,  _1 = 3 调用,从下面这个函数开始,


1 template <typename _T>
2 op<_T> place_holder::operator=(_T i);


那么这个_T 被推导为 int, 然后返回一个 op<int>, 然后 op<int> 里面就有一个 int op<int>::_i;


于是,在for_each里面,相当于有这么一句:


1 op<int> p;
2 p(*iter);


op的模板参数被推定义为int了(不是推导的,类模板不会推导)。


所以手法是先靠函数推导模板参数,再靠类保存类型信息。


于是,下面的函数模板


1 template<typename _T>
2 _T& op<int>::operator()(_T& i)
3 {
4     i = _i;
5 }


的模板参数 _T 就被推导成 *iter 的类型了,也就是容器的 value_type 了。


好了,到现在,一个赋值的lambda就做好了,它还能这么用:


1 double x;
2 (_1 = 5)(x);
就给x赋值5了,神奇吧。
因为  (_1 = 5)返回的是个lambda表达式,也就是个仿函数,:)


现在才看到lambda核心的一小部分,已经让人感觉眩晕了。
看看现在还存在的问题,只实现了一个赋值操作,其他的呢?
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638 欢迎加入本论坛超级QQ群:122538123
级别: 管理员
发帖
6294
金币
2497
威望
2887
贡献值
0
元宝
0
只看该作者 板凳  发表于: 01-19
看看之前做到哪里了:实现了一个赋值的lambda表达式。


这次来看看怎么添加新的运算进去,然后再说点关于表达式的问题,为以后的扩展打下理论基础。


先看看之前的代码吧,


1 template <typename _U>
2 struct op
3 {
4     op(_U i)
5         : _i(i)
6     {}
7
8     _U _i;
9
10     template<typename _T>
11     _T& operator()(_T& i)
12     {
13         i = _i;
14     }
15 };
16
17 struct place_holder
18 {
19     template <typename _T>
20     op<_T> operator=(_T i)
21     {
22         return op<_T>(i)
23     }
24 };
25
26 place_holder _1;
27
28 vector<double> v;
29 for_each(v.begin(), v.end(), _1 = 3);


现在,要在这个基础上,添加新的操作进来,比如说operator+=吧。要怎么做呢?


1.  place_holder要重载operator+= 才可以,因为place_holder的主要任务就是替我们生成一个仿函数。
2.  要有相应的仿函数来真正的做 += ,也就是说在仿函数的operator()里面,要有真正干活的操作。


好了,开始吧, 先看看实现,然后在解释。


1 struct op
2 {
3     op(int i)
4         : _i(i)
5     {}
6
7     int _i;
8
9     int operator()(int& i)
10     {
11         return i = _i;
12     }
13 };
14
15 struct op1
16 {
17     op1(int i)
18         : _i(i)
19     {}
20
21     int _i;
22
23     int operator()(int& i)
24     {
25         return i += _i;
26     }
27 };
28
29 struct place_holder
30 {
31     op operator=(int i)
32     {
33         return op(i);
34     }
35
36     op1 operator+=(int i)
37     {
38         return op1(i);
39     }
40 };
41
42 place_holder _1;
43
44 void main()
45 {
46
47     vector<int> v;
48     v.push_back(12);
49     v.push_back(1342);
50     v.push_back(23);
51
52     for_each(v.begin(), v.end(), _1 += 3);
53 }


好了,现在+=操作已经被支持了。多么简单呀。来看看都做了些什么:


1.  给place_holder增加了一个operator+=函数, operator+= 返回op1类型的仿函数。
2.  增加了一个op1的仿函数(类模板),用来真正的执行 += 的运算。


当编译器看到  _1 += 3 时,去找到 place_holder::operator+=, 然后把模板参数推导成 int,返回一个 op1<int> 对象。
在for_each里面,就调用op1<int>::operator+=了。


当然也可以这么用:


1 double x = 0.0;
2 (_1 += 10.4)(x);


到这里大家想必已经可以照猫画虎,实现其他操作了吧。但是当实现的操作多起来的时候,新的问题就来了,比如想要个 _1 = _2 + 3.0 的时候呢?看看下面的代码:


1 _1 = _2 + 3.0    //lambda表达式
2
3 void fun(double& lhs, const double& rhs)  //相同功能函数
4 {
5     lhs = rhs + 3.0;
6 }
7
8 struct op
9 {
10     template <typename _T>
11     _T operator(_T& lhs, const _T& rhs)
12     {
13         lhs = rhs + 3.0;
14     }
15 };
16


看看lambda表达式为我们省了多少代码!当然我不是为了说这个而写这么长段代码,我想说,那个op是我们的最终目标,能实现吗?不能!因为在op里面直接出现了3.0,按照前面的惯例,应该在op里面有一个成员变量来保存3.0,不是吗?根本问题不在这里。


仔细想想我们到底在做什么,我们在用template的技法,“编译”表达式。
place_holder其实就像C++的表达式,op就像汇编语言,通过template技法,把place_holder的表达式“编译”成用op组成的操作,op是可以直接被C++运行的仿函数。也就是说是一个从lambda语法到C++语法的编译器,但是这个编译器靠template技法实现,由真正的C++编译器进行模板推导,最后“编译”成C++的仿函数。所以一句话就是:


用template技法实现的从lambda语法到C++语法的“编译器”。


所以根本问题在于op的这种写法没有办法扩展,难道对于每种连起来的操作,都分别写一个op吗(比如_1 = (_2 + 3.0) * (_2 - 3.0),C++中表达式无数,要是每种都要写个op,那要lambda何用 )?op相当于汇编,只要几个简单的运算就OK,关键在于按照place_holder的表达,把op组合起来。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638 欢迎加入本论坛超级QQ群:122538123
级别: 管理员
发帖
6294
金币
2497
威望
2887
贡献值
0
元宝
0
只看该作者 地板  发表于: 01-19
Boost.Lambda是什么?
Boost Lambda库是C++模板库,以C++语言实现了lambda抽象.Lambda这个术语来自函数编程语言和lambda闭包理论,lambda抽象实际上定义了匿名函数.了解过C#新引入的匿数函数特性或Lisp编程的人,对这些概念理解会有很大帮助.Lambda库设计的主要动机是为STL算法提供灵活方便的定义匿名函数对象的机制.这个Lambda库究竟是有什么用呢?代码胜千言!看下面将STL容器中的元素打印到标准输出上的代码.


for_each(a.begin(), a.end(), std::cout << _1 << ' ');
表达式std::cout << _1 << ' '定义了一元函数对象.变量_1是函数的形参,是实参的占位符.每次for_each的迭代中,函数带着实际的参数被调用,实际参数取代了占位符,然后函数体里的内容被执行.Lambda库的核心就是让你能像上面所展示的那样,在STL算法的调用点,定义小的匿名函数对象.


Lambda库的安装
Lambda库只由头文件组成,这就意味着你不需要进行任何编译,连接,生成二进制库的动作,只需要boost库头文件路径包含进你的工程中即可使用.


与现代的C++语言一样,在使用时你需要声明用到的名字空间,把下列的代码包含在你的源文件头:


using namespace boost::lambda;
Boost Lambda库的动机
在标准模板库STL成为标准C++的一部分后,典型的STL算法对容器中元素的操作大都是通过函数对象(function objects)完成的.这些函数作为实参传入STL算法.


任何C++中以函数调用语法被调用的对象都是函数对象.STL对某些常见情况预置了些函数对象.比如:plus,less,not1下面就是标准plus模板的一种可能实现:
template <class T>
struct plus : public binary_function <T, T, T> {
  T operator()(const T& i, const T& j) const {
    return i + j;
  }
};
基类binary_function<T, T, T>包含了参数和函数对象返回类型的类型定义,这样可使得函数对象可配接.


除了上面提到的基本的函数对象外,STL还包含了binder模板,将可配接的二元函数中的某个实参固定为常量值,来创建一个一元函数对象.比如:
class plus_1 {
  int _i;
public:
  plus_1(const int& i) : _i(i) {}
  int operator()(const int& j) { return _i + j; }
};
上面的代码显性地创建了一个函数对象,将其参数加1.这样的功能可用plus模板与binder模板(bind1st来等效地实现.举例来说,下面的两行表达式创建了一个函数对象,当它被调用时,将返回1与调用参数的和.


plus_1(1)
bind1st(plus<int>(), 1)
plus<int>就是计算两个数之和的函数对象.bind1st使被调用的函数对象的第一个参数绑定到常量1.作为上面函数对象的使用示例,下面的代码就是将容器a中的元素加1后,输出到标准输出设备:


transform(a.begin(), a.end(), ostream_iterator<int>(cout),
          bind1st(plus<int>(), 1));
为了使binder更加通用,STL包含了适配器(adaptors)用于函数引用与指针,以及成员函数的配接.
所有这些工具都有一个目标,就是为了能在STL算法的调用点有可能指定一个匿名的函数,换句说,就是能够使部分代码片断作为参数传给调用算法函数.但是,标准库在这方面只做了部分工作.上面的例子说明用标准库工具进行匿名函数的定义还是很麻烦的.复杂的函数调用表达式,适配器,函数组合符都使理解变得困难.另外,在运用标准库这些方法时还有明显的限束.比如,标准C++98中的binder只允许二元函数的一个参数被绑定,而没有对3参数,4参数的绑定.这种情况在TR1实施后,引进了通用的binder后可能改善,对于使用MSVC的程序员,有兴趣还可以查看下微软针对VS2008发布的TR1增强包.


但是不管怎样,Lambda库提供了针对这些问题比较优雅的解决方法:


对匿名函数以直观的语义进行创建,上面的例子可改写成:


transform(a.begin(), a.end(), ostream_iterator<int>(cout),
          1 + _1);
更直观点:


for_each(a.begin(), a.end(), cout << (1 + _1));
绝大部分对函数参数绑定的限制被去除,在实际C++代码中可以绑定任意的参数


分离的函数组合操作不再需要了,函数组合被隐性地支持.


Lambda表达式介绍
Lambda表达在函数式编程语言中很常见.在不同语言中,它们的语法有着很大不同,但是lambda表达式的基本形式是:


lambda x1...xn.e
lambda表达式定义了匿名函数,并由下列的元素组成


函数的参数:x1...xn
表达式e,以参数x1...xn的形式计算函数的值
一个简单的lambda表达式的例子是:


(lambda x y.x+y) 2 3 = 2 + 3 = 5
在lambda表达式的C++版本中,表达式中x1...xn不需要,已预定义形式化的参数.在现在Boost.Lambda库中,存在三个这样的预定义的参数,叫做占位符:_1,_2,和_3.它们分别指代在lambda表达式中的第一,二,三个参数.比如,下面这样的lambda表达式:


lambda x y.x+y
C++定义的形式将会是这样:


_1 + _2
因此在C++中的lambda表达式没有语义上所谓的关键字.占位符作为运算符使用时就隐性地意味着运算符调用是个lambda表达式.但是只有在作为运算符调用才是这样.当Lambda表达式包含函数调用,控制结构,转换时就需要特殊的语法调用了.更为重要的是,作为函数调用是需封装成binder函数的形式.比如,下面这个lambda表达式:


lambda x y.foo(x,y)
不应写成foo(_1,_2),对应的C++结构应如下:


bind(foo, _1, _2)
对于这种表达式,更倾向于作为绑定表达式bind expressions


lambda表达式定义了C++的函数对象,因此,对于函数调用的形式跟其他的函数对象一样,比如:(_1 + _2)(i, j).


性能
性能,运行效率,总是C++程序员关心的话题.理论上,相对于手写循环代码,使用STL算法和Lambda函数对象的所有运行开销,可以通过编译优化消除掉.这种优化取决于编译器,实际中的编译器大都能做到.测试表明,性能会有下降,但是影响不大,对于代码的效率和简洁之间的权衡,只能由程序员自己做出判断了.


Lambda库的设计与实现中大量运用了模板技术,造成对于同一模板需要大量的递归实例化.这一因素可能使构建复杂逻辑的lambda表达式,不是一个非常理想的做法.因为编译这些表达式需要大量的内存,从而使编译时间变得非常慢,这在一些大型项目中会更加突出.还有在发生编误错误时,引发的大量错误信息,不能有效地指出真正错误之处.最后点,C++标准建议模板的嵌套层次不要超过17层来防止导致无限递归,而复杂的Lambda表达式模板会很容易超过这一限制.虽然大多数编译器允许更深层次的模板嵌套,但是通常需要显性地传入一个命令行参数才能做到.
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638 欢迎加入本论坛超级QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
如果您提交过一次失败了,可以用”恢复数据”来恢复帖子内容
认证码: