C++0x尝鲜:lambda表达式

关于C++0x

作为C++下一代标准的C++0x,其命名的本意无非是“200x年正式推出的C++新标准”,但目前(2010年)显然已没有可能,似乎改名为C++1x才名正言顺,不过为了避免不必要的混乱,C++标准委员会仍然坚持使用原名。
 

关于lambda表达式

在函数型语言(FP)中大行其道的lambda表达式被引入以“多范式”(multi-paradigm)为标签的C++语言应该是一件顺理成章的事。lambda表达式的引入对于改善C++语言中以STL算法为代表的FP编程范式可谓是大有裨益,事实上它也由此成为C++0x中最受欢迎的新特性之一。
 
在新标准中,lambda表达式将被编译器翻译成C++程序员早已熟悉的函数对象(function object),并替代后者行使“STL算法的参数”这一功能。下面用代码说明这一点:
 

代码1

#include <array> #include <iostream> using namespace std; int main() { array<int, 20> a; int n = 0; for(auto i = a.begin(); i != a.end(); i++) *i = n++; cout << n << endl; return 0; } //20
代码说明
  1. 代码1用连续的整数0-19来填充长度为20的数组a,最后打印用以填充数组的变量n来确认数组的长度。
  2. array容器是C++0x中新引入标准的STL容器,其功能相当于原生数组+STL容器规范。
    array<int, 20> a; 可以被大致看作 int a[20];
  3. auto关键字在C++0x中被赋予新的职责:类型自动推导。
    在 auto i = a.begin(); 这句语句中,变量 i 的类型与其初始化表达式 a.begin() 相同,即array<int, 20>::iterator
    若不使用auto关键字,这句语句将不得不写成冗长的 array<int, 20>::iterator i = a.begin();
 

代码2

#include <array> #include <algorithm> #include <iostream> using namespace std; int gn = 0; int next_int() { return gn++; } int main() { array<int, 20> a; generate(a.begin(), a.end(), next_int); cout << gn << endl; return 0; } //20
代码说明
  1. 代码2改用FP编程范式来完成同样的功能,具体来说它使用了STL库的generate算法来填充数组a。
  2. 代码2中的generate算法用全局函数 next_int 来充当它的生成器。
    由于作为生成器的全局函数 next_int 不能接受额外的参数,代码1中局部变量 n 的功能不得已改用全局变量 gn 来完成。
  3. 相比较代码1,
    代码2的优点在于:用generate算法明确了代码的意图,提高了代码可读性。
    其缺点在于:算法的可变部分(即生成器代码全局函数 next_int )与算法调用表达式相距过远,另外所使用的全局函数 next_int 以及全局变量 gn 也污染了全局命名空间。
    注:若局部函数(local function)被引入C++0x(正在讨论中),这两个问题即可得到解决。
 

代码3

#include <array> #include <algorithm> #include <iostream> using namespace std; class generator { public: explicit generator(int& n) : n_(n) {} int operator()() const { return n_++; } private: int& n_; }; int main() { array<int, 20> a; int n = 0; generate(a.begin(), a.end(), generator(n)); cout << n << endl; return 0; } //20
代码说明
  1. 代码3同样使用generate算法来填充数组a,所不同的是它用函数对象“generator类的实例”来充当算法的生成器。
  2. 鉴于函数对象类 generator 可以保存并改变状态,代码1中局部变量 n 的功能改由 generator 类的成员变量 n_ 来完成。
  3. 相比较代码2,
    代码3的优点在于:不必使用全局变量来保存状态,提高了代码的可维护性。
    但其缺点也很明显:算法可变部分(即生成器代码generator 类)不仅过于冗长,而且与算法调用表达式分离的问题也没有得到解决。
    注:由于C++0x中函数对象已经可以在函数内声明,代码3中的generator类也可以作为局部类在main中声明,如此便解决了全局命名空间被污染的问题。
 

代码4

#include <array> #include <algorithm> #include <boost/lambda/lambda.hpp> #include <iostream> using namespace std; int main() { array<int, 20> a; int n = 0; generate(a.begin(), a.end(), boost::lambda::var(n)++); cout << n << endl; return 0; } //20
代码说明
  1. 代码4同样使用generate算法来填充数组a,不过这一次算法的生成器来源于Boost.Lambda类库。
  2. 代码4中Lambda类库中的var函数用于捕获本地变量n并生成函数对象,运算符++被重载以生成更大的函数对象。
    简单来讲,Lambda类库就是一个函数对象的生成器。
  3. 代码4克服了代码2,3的缺点:算法可变部分(即生成器代码var(n)++)不仅简短而且就处在算法调用表达式之内。
    不过代码4也产生了新的问题:
    使用者必须学习并熟悉Lambda类库,由于这个类库包含了大量新的概念及用法,学习曲线较为陡峭。
    Lambda类库使用了较为高级的表达式模板技术,受C++语言本身的限制,有时所生成的lambda表达式并不太直观,降低了可读性。
    另外出于同样的原因,大量使用Lambda类库的代码还存在编译速度慢、一旦用错出错信息难以辨读等缺点。
 

代码5

#include <array> #include <algorithm> #include <iostream> using namespace std; int main() { array<int, 20> a; int n = 0; generate(a.begin(), a.end(), [&]{return n++;}); //generate(a.begin(), a.end(), [&]()->int{return n++;}); //generate(a.begin(), a.end(), [&n]{return n++;}); //generate(a.begin(), a.end(), [=]() mutable {return n++;}); //generate(a.begin(), a.end(), [n]() mutable {return n++;}); //for_each(a.begin(), a.end(), [](int& v){v *= 2;}); //for_each(a.begin(), a.end(), [](int& v)->void{v *= 2;}); cout << n << endl; return 0; } //20
代码说明
  1. 代码5同样使用generate算法来填充数组a,不过这一次算法的生成器来源于本文的主题:作为C++0x标准新特性的lambda表达式。
  2. 代码5中 [&]{return n++;} 部分就是运用了新特性的lambda表达式。
    如果加上语法中可省略的部分,该lambda表达式应为:[&]()->int{return n++;} 。
    (即将第12行替换成第13行,并反注释)
  3. [&]为lambda表达式的导入符(introducer),&的意思为引用捕获所有局部变量,即将lambda表达式中用到的所有局部变量的引用传入lambda表达式中。
  4. 由于此处lambda表达式的函数体 {return n++;} 中只用到局部变量n,所以也可以将导入符 [&] 改成 [&n] 表示只捕获局部变量 n 的引用。
    (即将第12行替换成第14行)
  5. 与引用捕获相对应的是值捕获,即将lambda表达式中用到的所有局部变量的值拷贝并传入lambda表达式中。
    若采用值捕获,导入符[&]须改为[=],局部变量n的值将被拷贝进入lambda表达式中,数组a的内容不受影响,但程序最后输出为0。
    (即将第12行替换成第15行)
    注意:值捕获而拷贝进入lambda表达式中的局部变量缺省情况下不可修改,若要修改须使用mutable关键字。
  6. 由于此处lambda表达式的函数体 {return n++;} 中只用到局部变量n,所以也可以将导入符 [=] 改成 [n] 表示只捕获局部变量 n 的值。
    (即将第12行替换成第16行)
  7. 如果lambda表达式无需捕获任何局部变量,导入符可用 [] ,参见第17行。
    第17行代码(反注释后)调用for_each算法将所有数组成员的值乘以2。
  8. lambda表达式的导入符之后为参数列表部分,这部分与普通函数的参数列表大致相同。
    注意:lambda表达式参数列表部分的参数不可省略变量名,也不允许有缺省值。
    第12行的generate算法中lambda表达式没有参数,所以这部分是 () 。
    注意:空参数列表在返回值类型等中间部分省略的情况下也可省略。
    第17行的for_each算法中需要将数组成员的引用传给lambda表达式,所以这部分是 (int& v) 。
  9. lambda表达式的中间部分包括mutable关键字、异常规范、返回值类型三部分内容。
  10. lambda表达式的返回值类型采用C++0x标准新引入的后置语法,即 ->T ,某些情况下可省略。
  11. lambda表达式的函数体与普通函数一样放在最后,在以下两种情况下返回值类型可省略:
    函数体只包括一句语句:return expression; 此时函数返回值类型与expression相同,参见第12行(省略返回值类型)和第13行(不省略返回值类型)。
    函数体不返回任何值,即返回值类型为void,参见第17行(省略返回值类型)和第18行(不省略返回值类型)。
  12. 与代码4相比,代码5不仅保持了算法可变部分(即生成器代码[&]{return n++;})短小精悍以及与算法调用不分离等优点,而且在很大程度上还克服了其难于学习、不够直观以及编译速度慢的缺点。
    由此不难得出结论:使用了新特性lambda表达式的代码5就是这五种方案中的最佳选择。
  13. 代码5中的lambda表达式经编译器处理后将被翻译成类似于代码3中的函数对象类及其调用,
    但在实际效果上lambda表达式可以被看作C/C++语言中尚不具备的局部函数(local function)。
    注:真正的local function能否进入C++0x尚在讨论之中,目前不被看好。
 

补记

代码2,3,5部分摘自
http://channel9.msdn.com/posts/kmcgrath/Lambda-Expressions-in-C/
(已作修改)。
 
相关链接
C++0x wiki http://en.wikipedia.org/wiki/C%2B%2B0x
C++0x FAQ http://www2.research.att.com/~bs/C++0xFAQ.html
VC10中的lambda http://blogs.msdn.com/vcblog/archive/2008/10/28/lambdas-auto-and-static-assert-c-0x-features-in-vc10-part-1.aspx
 

你可能感兴趣的:(C++,算法,function,iterator,lambda,generator)