找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 3923|回复: 0

VC10和C++ 0x (1) - lambda表达式

[复制链接]
发表于 2012-2-22 15:33:31 | 显示全部楼层 |阅读模式
【本文大部分内容译自Visual C++ Team Blog】

尽管C++社区对C++ 0x很是追捧,但是各厂商对于新标准的支持并不热乎。盼星星盼月亮,微软作为Windows平台上最强势的C++编译器厂商也终于在Visual Studio 2010中开始支持C++ 0x的特性。

Lambda表达式,auto 和静态断言(static_assert)
Visual Studio 2010中的Visual C++编译器,即VC10, 包含了4个C++ 0x的语言特性 - lambda表达式,auto,static_assert 和 rvalue reference (右值引用).


相关链接:

lambdas
使用过函数式编程语言(如lisp, F#)或一些动态语言(如Python,Javascript)的大侠对于lambda表达式一定不会陌生。
在C++ 0x中,引入了lambda表达式来定义无名仿函数。下面是一个lambda表达式的简单例子:
// File: meow.cpp
  1. #include <algorithm>
  2. #include <iostream>
  3. #include <ostream>
  4. #include <vector>
  5. using namespace std;
  6. int main() {
  7.     vector<int> v;
  8.     for (int i = 0; i < 10; ++i) {
  9.         v.push_back(i);
  10.     }
  11.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  12.     cout << endl;   
  13.     return 0;
  14. }
复制代码

C:\Temp>cl /EHsc /nologo /W4 meow.cpp > NUL && meow
0 1 2 3 4 5 6 7 8 9

for_each一行中,中括号[]称为lambda introducer, 它告诉编译器接下来的是一个lambda表达式;接下来(int n)是lambda表达式的参数声明;最后大括号里边就是“函数体”了。
注意这里因为lambda表达式生成的是functor,所以“函数体”实际上是指这个functor的operator ()的调用部分。你也许会问:那么返回值呢?缺省情况下lambda表达式生成的functor调用
返回类型为void.


#include <algorithm>
所以,可以理解为上边的代码会被编译器翻译成如下:
  1. #include <iostream>
  2. #include <ostream>
  3. #include <vector>
  4. using namespace std;
  5. struct LambdaFunctor {
  6.     void operator()(int n) const {
  7.         cout << n << " ";
  8.     }
  9. };
  10. int main() {
  11.     vector<int> v;
  12.     for (int i = 0; i < 10; ++i) {
  13.         v.push_back(i);
  14.     }
  15.     for_each(v.begin(), v.end(), LambdaFunctor());
  16.     cout << endl;
  17.     return 0;
  18. }
复制代码
为了方便,以下会用"lambda返回void"的简短表述来代替冗长啰嗦的表述:lambda表达式生成一个functor类型,这个functor类型的函数调用操作符(operator())返回的类型是void.
请大家一定记住:lambda表达式生成了类型,并构造该类型的实例。

#include <algorithm>
下面的例子中lambda表达式的“函数体”包含多条语句:
  1. #include <iostream>
  2. #include <ostream>
  3. #include <vector>
  4. using namespace std;
  5. int main() {
  6.     vector<int> v;
  7.     for (int i = 0; i < 10; ++i) {
  8.         v.push_back(i);
  9.     }
  10.     for_each(v.begin(), v.end(), [](int n) {
  11.         cout << n;
  12.         if (n % 2 == 0) {
  13.             cout << " even ";
  14.         } else {
  15.             cout << " odd ";
  16.         }
  17.     });
  18.     cout << endl;
  19.     return 0;
  20. }
复制代码

#include <algorithm> 上文提到了lambda表达式缺省情况下返回void. 那么如果需要返回其他类型呢?
答案是:lambda表达式的“函数体”中如果有一个return的表达式,例如{ return expression; },那么编译器将自动推演expression的类型作为返回类型。
  1. #include <deque>
  2. #include <iostream>
  3. #include <iterator>
  4. #include <ostream>
  5. #include <vector>
  6. using namespace std;
  7. int main() {
  8.     vector<int> v;
  9.     for (int i = 0; i < 10; ++i) {
  10.         v.push_back(i);
  11.     }
  12.     deque<int> d;
  13.     transform(v.begin(), v.end(), front_inserter(d), [](int n) { return n * n * n; });
  14.     for_each(d.begin(), d.end(), [](int n) { cout << n << " "; });
  15.     cout << endl;
  16. }
复制代码
上例中返回值n * n * n很简单,类型推演是显而易见的。但是如果lambda表达式中有非常复杂的表达式时,编译器可以无法推演出其类型,或者是推演出现二义性,这时候你可以
显式地指明返回值类型。如下所示:
  1. transform(v.begin(), v.end(), front_inserter(d), [](int n) -> double {
  2.     if (n % 2 == 0) {
  3.         return n * n * n;
  4.     } else {
  5.         return n / 2.0;
  6.     }
  7. });
复制代码

黑体部分中有的“-> double”显式地指明了lambda表达式的返回类型是double.

以上例子中的lambda都是无状态的(stateless),不包含任何数据成员。很多时候我们需要lambda包含数据成员以保存状态,这一点可以通过“捕获”(capturing)局部变量来实现。
lambda表达式的导入符(lambda-introducer)是空的,也就是“[]”,表明该lambda是一个无状态的。但是在lambda导入符中可以指定一个“捕获列表”(capture-list)。



  1. int main() {
  2.     vector<int> v;
  3.     for (int i = 0; i < 10; ++i) {
  4.         v.push_back(i);
  5.     }
  6.     int x = 0;
  7.     int y = 0;
  8.     cout << "Input: ";
  9.     cin >> x >> y;
  10.     v.erase(remove_if(v.begin(), v.end(), [x, y](int n) { return x < n && n < y; }), v.end());
  11.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  12.     cout << endl;
  13. }
复制代码

上边的代码中的lambda使用了局部变量x和y,将值介于x和y之间的元素从集合中删除。
程序运行示例如下 -
Input: 4 7
0 1 2 3 4 7 8 9


class LambdaFunctor {
上边的代码可以理解为:
  1. public:
  2.     LambdaFunctor(int a, int b) : m_a(a), m_b(b) { }
  3.     bool operator()(int n) const { return m_a < n && n < m_b; }
  4. private:
  5.     int m_a;
  6.     int m_b;
  7. };
  8. int main() {
  9.     vector<int> v;
  10.     for (int i = 0; i < 10; ++i) {
  11.         v.push_back(i);
  12.     }
  13.     int x = 0;
  14.     int y = 0;
  15.     cout << "Input: ";
  16.     cin >> x >> y;
  17.     v.erase(remove_if(v.begin(), v.end(), LambdaFunctor(x, y)), v.end());
  18.     copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
  19.     cout << endl;
  20. }
复制代码
上面代码中很重要的一点信息是:lambda中捕获的局部变量是以“传值”的方式传给匿名函数对象的。在匿名函数对象中,保存有“捕获列表”中局部变量的拷贝。
这一点使得匿名函数对象的生命周期能够长于main中的x,y局部变量。然而这样的传值方式带来几个限制:

  • lambda中的这两个拷贝并不能被改变,因为缺省情况下函数对象的operator()是const;
  • 有的对象的拷贝操作开销很大或者不可能(例如如果上面代码中的x, y是数据库链接或者某个singleton)
  • 即使在lambda内部修改了m_a, m_b也不能够影响外边main函数中的x和y

既然有了“传值”,你一定猜到了还会有“传引用”。bingo! 你是对的。
在讨论“传引用”之前,我们先来看看另一个比较有用的东西。假设你有一大堆的局部变量需要被lambda使用,那么你的“捕获列表”将会写的很长,这肯定不是件愉快的事情。
好在C++委员会的老头们也想到了,C++ 0x中提供了一个省心的东西:如果捕获列表写成 [=],表示lambda将捕获所有的局部变量,当然也是传值方式。这种方式姑且被称为“缺省捕获”(capture-default)。

  1. int main() {
  2.     vector<int> v;
  3.     for (int i = 0; i < 10; ++i) {
  4.         v.push_back(i);
  5.     }
  6.     int x = 0;
  7.     int y = 0;
  8.     cout << "Input: ";
  9.     cin >> x >> y; // EVIL!
  10.     v.erase(remove_if(v.begin(), v.end(), [=](int n) { return x < n && n < y; }), v.end());
  11.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  12.     cout << endl;
  13. }
复制代码

当编译器在lambda的作用范围内看到局部变量x, y时,它会以传值的方式从main函数中将他们捕获。
下面我们来看如何突破前面提到的3点限制。

第一点,修改lambda表达式中的局部变量拷贝(e.g. m_a, m_b)
缺省情况下,lambda的operator ()是const 修饰的,但是你可以使用mutable关键字改变这一点。

  1. int main() {
  2.     vector<int> v;
  3.     for (int i = 0; i < 10; ++i) {
  4.         v.push_back(i);
  5.     }
  6.     int x = 1;
  7.     int y = 1;
  8.     for_each(v.begin(), v.end(), [=](int& r) mutable {
  9.         const int old = r;
  10.         r *= x * y;
  11.         x = y;
  12.         y = old;
  13.     });
  14.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  15.     cout << endl;
  16.     cout << x << ", " << y << endl;
  17. }
复制代码

代码运行结果如下
0 0 0 6 24 60 120 210 336 504
1, 1

这里我们解决了第一个限制,但是却产生了一个新的限制
4.  lambda中对捕获变量的修改并不会影响到main函数中的局部变量,因为lambda捕获局部变量使用的是传值方式

下面该“传引用”的方式登场了,它能够有效地解决2,3,4三个限制。

传引用的语法为: lambda-introducer [&x, &y]
这里的捕获列表应该理解为:X& x, Y& y ; 因为我们实际上是取的x,y的引用而不是地址。

  1. int main() {
  2.     vector<int> v;
  3.     for (int i = 0; i < 10; ++i) {
  4.         v.push_back(i);
  5.     }
  6.     int x = 1;
  7.     int y = 1;
  8.     for_each(v.begin(), v.end(), [&x, &y](int& r) {
  9.         const int old = r;
  10.         r *= x * y;
  11.         x = y;
  12.         y = old;
  13.     });
  14.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  15.     cout << endl;
  16.     cout << x << ", " << y << endl;
  17. }
复制代码
运行结果如下 -
0 0 0 6 24 60 120 210 336 504
8, 9

上面代码会被编译器“翻译”成:
  1. #pragma warning(push)
  2. #pragma warning(disable: 4512) // assignment operator could not be generated
  3. class LambdaFunctor {
  4. public:
  5.     LambdaFunctor(int& a, int& b) : m_a(a), m_b(b) { }
  6.     void operator()(int& r) const {
  7.         const int old = r;
  8.         r *= m_a * m_b;
  9.         m_a = m_b;
  10.         m_b = old;
  11.     }
  12. private:
  13.     int& m_a;
  14.     int& m_b;
  15. };
  16. #pragma warning(pop)
  17. int main() {
  18.     vector<int> v;
  19.     for (int i = 0; i < 10; ++i) {
  20.         v.push_back(i);
  21.     }
  22.     int x = 1;
  23.     int y = 1;
  24.     for_each(v.begin(), v.end(), LambdaFunctor(x, y));
  25.     copy(v.begin(), v.end(), ostream_iterator<int>(cout, " "));
  26.     cout << endl;
  27.     cout << x << ", " << y << endl;
  28. }
复制代码

注意:当你使用lambda时,VC10编译器会为lambda的定义部分自动禁用C4512警告。
当以传引用方式捕获局部变量时,lambda的函数对象在自己内部以引用方式保存main函数中的局部变量。
当然因为使用的是局部对象的引用,使用lambda表达式时一定要注意不能够超出局部变量的生命周期。

和上文提高的[=]类似,我们可以用[&]来以“传引用”的方式捕获所有的局部变量。

到目前为止,局部变量的捕获方式要么是“值语义”要么是“引用语义”,那么可以混合这两种方式吗?可以!
例如:[a, b, c, &d, e, &f, g],其中变量d和f是按引用语义捕获,而a,b,c,e和g是按值语义捕获。

另外很有用的一点是:你可以指定一个缺省捕获(capture-default),然后重载(override)某些局部变量的捕获方式。
下边例子中[=, &sum, &product]告诉编译器用值语义方式捕获所有的局部变量,但是有两个例外 - sum和product是按引用语义来捕获。

  1. int main() {
  2.     vector<int> v;
  3.     for (int i = 0; i < 10; ++i) {
  4.         v.push_back(i);
  5.     }
  6.     int sum = 0;
  7.     int product = 1;
  8.     int x = 1;
  9.     int y = 1;
  10.     for_each(v.begin(), v.end(), [=, &sum, &product](int& r) mutable {
  11.         sum += r;
  12.         if (r != 0) {
  13.             product *= r;
  14.         }
  15.         const int old = r;
  16.         r *= x * y;
  17.         x = y;
  18.         y = old;
  19.     });
  20.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  21.     cout << endl;
  22.     cout << "sum: " << sum << ", product: " << product << endl;
  23.     cout << "x: " << x << ", y: " << y << endl;
  24. }
复制代码

运行结果如下 -
0 0 0 6 24 60 120 210 336 504
sum: 45, product: 362880
x: 1, y: 1

再来看看下边的代码 - 在lambda中使用类成员变量

  1. class Kitty {
  2. public:
  3.     explicit Kitty(int toys) : m_toys(toys) { }
  4.     void meow(const vector<int>& v) const {
  5.         for_each(v.begin(), v.end(), [m_toys](int n) {
  6.             cout << "If you gave me " << n << " toys, I would have " << n + m_toys << " toys total." << endl;
  7.         });
  8.     }
  9. private:
  10.     int m_toys;
  11. };
  12. int main() {
  13.     vector<int> v;
  14.     for (int i = 0; i < 3; ++i) {
  15.         v.push_back(i);
  16.     }
  17.     Kitty k(5);
  18.     k.meow(v);
  19. }
复制代码

不幸的是,编译这段代码将产生这样的错误:
error C3480: 'Kitty::m_toys': a lambda capture variable must be from an enclosing function scope
为什么呢?lambda表达式能够让你捕获局部变量,但是类的数据成员并不是局部变量。
解决方案呢?别着急。lambda为捕获类的数据成员大开方便之门,你可以捕获this指针。

  1. class Kitty {
  2. public:
  3.     explicit Kitty(int toys) : m_toys(toys) { }
  4.     void meow(const vector<int>& v) const {
  5.         for_each(v.begin(), v.end(), [this](int n) {
  6.             cout << "If you gave me " << n << " toys, I would have " << n + m_toys << " toys total." << endl;
  7.         });
  8.     }
  9. private:
  10.     int m_toys;
  11. };
  12. int main() {
  13.     vector<int> v;
  14.     for (int i = 0; i < 3; ++i) {
  15.         v.push_back(i);
  16.     }
  17.     Kitty k(5);
  18.     k.meow(v);
  19. }
复制代码

运行结果 -
If you gave me 0 toys, I would have 5 toys total.
If you gave me 1 toys, I would have 6 toys total.
If you gave me 2 toys, I would have 7 toys total.

当lambda表达式捕获“this”时,编译器看到m_toys后会在this所指向对象的范围内进行名字查找,m_toys被隐式地推演为this->m_toys。当然你也可以让编译器省省力气。显式地在
捕获列表中使用 this->m_toys。

lambda比较智能,你也可以隐式地捕获this指针。如下所示:

  1. class Kitty {
  2. public:
  3.     explicit Kitty(int toys) : m_toys(toys) { }
  4.     void meow(const vector<int>& v) const {
  5.         for_each(v.begin(), v.end(), [=](int n) {
  6.             cout << "If you gave me " << n << " toys, I would have " << n + m_toys << " toys total." << endl;
  7.         });
  8.     }
  9. private:
  10.     int m_toys;
  11. };
  12. int main() {
  13.     vector<int> v;
  14.     for (int i = 0; i < 3; ++i) {
  15.         v.push_back(i);
  16.     }
  17.     Kitty k(5);
  18.     k.meow(v);
  19. }
复制代码
运行结果:
If you gave me 0 toys, I would have 5 toys total.
If you gave me 1 toys, I would have 6 toys total.
If you gave me 2 toys, I would have 7 toys total.

注意你也可以在上面代码中用 [&],但是结果是一样的 - this指针永远是按值语义被传递(捕获)的。你也不能够使用 [&this],呵呵。
如果你的lambda表达式是没有参数的,那么lambda表达式的导入符后边的括号()也可以省掉。例如:

  1. int main() {
  2.     vector<int> v;
  3.     int i = 0;
  4.     generate_n(back_inserter(v), 10, [&] { return i++; });
  5.     for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  6.     cout << endl;
  7.     cout << "i: " << i << endl;
  8. }
复制代码
运行结果如下:
0 1 2 3 4 5 6 7 8 9
i: 10

上边是 [&]() { return i++; }的简写形式。个人认为省掉括号并不是什么好的coding style。
下面是纯粹搞笑的写法:

  1. int main() {
  2.     [](){}();
  3.     []{}();
  4. }
复制代码

注意lambda的语法为:
( lambda-parameter-declaration-listopt ) mutableopt exception-specificationopt lambda-return-type-clauseopt

所以如果你需要用到mutable或者指定lambda的返回类型,空的括号就不能够省略了。
最后尽然lambda表达式生成是普通的函数对象,所以函数对象支持的用法lambda都支持。例如和tr1的function一起使用,
看看下边的代码,是不是很酷?

  1. using namespace std;
  2. using namespace std::tr1;
  3. void meow(const vector<int>& v, const function<void (int)>& f) {
  4.     for_each(v.begin(), v.end(), f);
  5.     cout << endl;
  6. }
  7. int main() {
  8.     vector<int> v;
  9.     for (int i = 0; i < 10; ++i) {
  10.         v.push_back(i);
  11.     }
  12.     meow(v, [](int n) { cout << n << " "; });
  13.     meow(v, [](int n) { cout << n * n << " "; });
  14.     function<void (int)> g = [](int n) { cout << n * n * n << " "; };
  15.     meow(v, g);
  16. }
复制代码

运行结果:
0 1 2 3 4 5 6 7 8 9
0 1 4 9 16 25 36 49 64 81
0 1 8 27 64 125 216 343 512 729

【THE END】

来自:http://www.cnblogs.com/brucejia/archive/2009/09/05/1560675.html


您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-12-22 18:10 , Processed in 0.016538 second(s), 6 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表