代码改变世界

转:C++ Lambda表达式

2012-07-09 13:28  youxin  阅读(1321)  评论(0编辑  收藏  举报

Visual Studio 2010(下称VS2010)中的Visual C++编译器包含了对4项(正式发布后可能更多)C++0x 特性的支持,分别为lambda表达式auto关键字static_assert,和右值引用(rvalue references)。此篇文章将对前三项进行详细解释,rvalue references将在后续文章中解释。

相关资料:
C++0x language feature status:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2705.html
C++0x library feature status:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2706.html
C++0x Working Draft:
http://open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2798.pdf

Lambda表达式

在C++0x中“lambda表达式”隐式的定义并构造了一个匿名的函数对象,它和普通的函数对象的行为是类似的。下面是“Hello World”的lambda:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;

int main() 
{
    vector<int> v;
    for (int i = 0; i < 10; ++i) 
    {
        v.push_back(i);
    }
    for_each(v.begin(),v.end(),[](int i) { cout<<i<<ends; } );
}

  代码中的“[]”叫做“lambda前导符”,它告诉编译器一个lambda表达式开始。而“(int n)”是lambda表达式中的参数声明部分,它告诉编译器,这个匿名的函数对象中,函数调用运算符(“()”运算符)调用时接受的参数。最后“{ cout << n << " "; }”大括号语句作为这个匿名函数对象的函数调用符的函数体。默认情况下,匿名函数对象的返回值为void

因此,上面的C++0x代码实质上等价于下面的C++98代码:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;

struct LambdaFunctor {
    void operator()(int n) const {
        cout << n << " ";
    }
};

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    for_each(v.begin(), v.end(), LambdaFunctor());
    cout << endl;
}

  现在,我不再说“那个匿名的函数对象的函数调用运算符的返回值是void”,而是直接说“这个lambda返回void”,但是,非常重要的一点是:一定要记住lambda表达式干了什么:定义了一个类(LambdaFunctor),然后构造了它的对象

当然了,lambda的大括号表达式中,可以包含多个语句:

 
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;

int main() {
    vector v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    for_each(v.begin(), v.end(), [](int n) {
        cout << n;

        if (n % 2 == 0) {
            cout << " even ";
        } else {
            cout << " odd ";
        }
    });

    cout << endl;
}

0 even 1 odd 2 even 3 odd 4 even 5 odd 6 even 7 odd 8 even 9 odd

  现在,lambda也不必总是返回void,如果一个lambda的大括号表达式为{ return expression; },那么这个lambda的返回值类型将被自动推断为expression的类型.

 

#include <algorithm>
#include <deque>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    deque<int> d;

    transform(v.begin(), v.end(), front_inserter(d), [](int n) { return n * n * n; });

    for_each(d.begin(), d.end(), [](int n) { cout << n << " "; });
    cout << endl;

}

 

729 512 343 216 125 64 27 8 1 0

这里,n * n * n的类型为int,所以,lambda返回值类型为int。

 有些更加复杂的lambda表达式无法做出类型推断,你需要显式的指定它

#include <algorithm>
#include <deque>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    deque<int> d;

    transform(v.begin(), v.end(), front_inserter(d), [](int n) -> double {
        if (n % 2 == 0) {
            return n * n * n;
        } else {
            return n / 2.0;
        }
    });

    for_each(d.begin(), d.end(), [](double x) { cout << x << " "; });
    cout << endl;
}

照道理应该输出:

4.5 512 3.5 216 2.5 64 1.5 8 0.5 0

可是vs2010输出了:

4 512 3 216 2 64 1 8 0 0
请按任意键继续. . .

说明vs2010对lambda的支持还不是很好

   “-> double”是可选的“lambda返回值类型从句”。为什么返回值类型声明没有像以前声明函数的时候放在前面呢?因为如果不把“[]”放在最前面的话,编译器将无法知道这是一个lambda表达式。

这里,如果你忘记了声明返回值类型,编译器将会报告错误

 error C3499: a lambda that has been specified to have a void return type cannot return a value

vs2010: error C3499: 已指定返回类型为 void 的 lambda 无法返回值

 

  上面列出的所有lambda都是无状态的:它们构造出来的匿名函数对象不包含任何数据成员。你也可以获得有状态的lambda,方法是通过“捕获”局部变量。空白的lambda前导符“[]”表示这是一个无状态的lambda,但是在“[]”内部,你可以指定一个捕获组

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    int x = 0;
    int y = 0;

    // op>>()不会读取输入流中最后一个回车换行符,
    // 这有时候会带来很多问题。我推荐
    // 避免使用它,而使用非成员函数
    // getline(cin, str)读取一整行,
    // 然后再解析它。但是为了简洁起见,
    // 我会继续使用 op>>():

    cout << "Input: ";
    cin >> x >> y;

    v.erase(remove_if(v.begin(),v.end(),[x,y](int n) { return x<n&&y>n; } ),v.end());

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;
}

Input: 4 7
0 1 2 3 4 7 8 9  (因为移除了4到7之间的元素)

如果你忘记了写捕获组,编译器会报告错误:

 error C3493: 'x' cannot be implicitly captured as no default capture mode has been specified

(一会儿我将会介绍默认捕获)

 记住,lambda表达式隐式的定义了一个匿名的函数对象,“{ return x < n && n < y; }”就是它函数调用符的函数体,所以尽管看起来大括号语句是在main()的范围内,但在概念上它是在main()函数范围之外的,lambda表达式中不能使用没有捕获的main()函数内的局部变量

这是把上面程序等价转换后的代码:

#include <algorithm>
#include <iostream>
#include <iterator>
#include <ostream>
#include <vector>
using namespace std;

class LambdaFunctor {
public:
    LambdaFunctor(int a, int b) : m_a(a), m_b(b) { }

    bool operator()(int n) const { return m_a < n && n < m_b; }

private:
    int m_a;
    int m_b;
};

int main() {
    vector<int>  v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    int x = 0;
    int y = 0;

    cout << "Input: ";
    cin >> x >> y; // EVIL!

    v.erase(remove_if(v.begin(), v.end(), LambdaFunctor(x, y)), v.end());

    copy(v.begin(), v.end(), ostream_iterator(cout, " "));
    cout << endl;
}

Input: 4 7
0 1 2 3 4 7 8 9

  这里,你可以清楚的看到捕获变量是按值传递的。局部变量的拷贝被存储在了函数对象中。这样,函数对象存活的时间可以比被捕获的局部变量长。但是,需要注意几点:

  (a)捕获的变量在lambda中不可修改,因为默认情况下生成的是const函数调用运算符;

   (b)有些对象的复制操作开销比较大;

   (c)局部变量的更新将不会影响lambda内已经被捕获过的副本(这符合通常的值传递语义)。一会儿,我将解释在需要的时候如何应对上述三种情况。

但是首先,如果你想捕获所有的局部变量,可以不必逐项列出它们,而只需告诉编译器“按值传递捕获所有变量”。实现方式是使用前导符“[=]”:

#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;

int main() {
    vector v;

    for (int i = 0; i < 10; ++i) {
        v.push_back(i);
    }

    int x = 0;
    int y = 0;

    cout << "Input: ";
    cin >> x >> y; // EVIL!

    v.erase(remove_if(v.begin(), v.end(), [=](int n) { return x < n && n < y; }), v.end());

    for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
    cout << endl;
}

Input: 4 7
0 1 2 3 4 7 8 9

当编译器看到lambda中的x和y,会将它们从main()中按值捕获进来

 对于情况(a)无法修改捕获的变量,如何处理呢?默认情况下,lambda的函数调用运算符是const的,但是你可以通过mutable关键字将其变为非const:

http://hi.baidu.com/icexile/blog/item/a9836460f05fcbd58cb10dd0.html

http://technet.microsoft.com/zh-cn/subscriptions/dd293603(v=vs.110).aspx

http://developer.51cto.com/art/201207/345807.htm

http://www.devbean.info/2012/05/cpp11-lambda/

http://www.cppblog.com/len/archive/2011/04/22/50286.html

 

 http://www.cnblogs.com/haippy/archive/2013/05/31/3111560.html

 上面网址部分内容:

C++11 的 lambda 表达式规范如下:

 

[ capture ] ( params ) mutable exception attribute -> ret { body } (1)  
[ capture ] ( params ) -> ret { body } (2)  
[ capture ] ( params ) { body } (3)  
[ capture ] { body } (4)  

 

其中

 

  • (1) 是完整的 lambda 表达式形式,
  • (2) const 类型的 lambda 表达式,该类型的表达式不能改捕获("capture")列表中的值。
  • (3)省略了返回值类型的 lambda 表达式,但是该 lambda 表达式的返回类型可以按照下列规则推演出来:
    • 如果 lambda 代码块中包含了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
    • 如果没有 return 语句,则类似 void f(...) 函数。
  • 省略了参数列表,类似于无参函数 f()。

 

mutable 修饰符说明 lambda 表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获对象的 non-const 方法。

 

exception 说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f(throw(X, Y)。

 

attribute 用来声明属性。

 

另外,capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:

 

    • [a,&b] a变量以值的方式呗捕获,b以引用的方式被捕获。
    • [this] 以值的方式捕获 this 指针。
    • [&] 以引用的方式捕获所有的外部自动变量。
    • [=] 以值的方式捕获所有的外部自动变量。
    • [] 不捕获外部的任何变量。
    • 例子:
    • #include <vector>
      #include <iostream>
      #include <algorithm>
      #include <functional>
       
      int main()
      {
          std::vector<int> c { 1,2,3,4,5,6,7 };
          int x = 5;
          c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; } ), c.end());
       
          std::cout << "c: ";
          for (auto i: c) {
              std::cout << i << ' ';
          }
          std::cout << '\n';
       
          // the type of a closure cannot be named, but can be inferred with auto
          auto func1 = [](int i) { return i+4; };
          std::cout << "func1: " << func1(6) << '\n'; 
       
          // like all callable objects, closures can be captured in std::function
          // (this may incur unnecessary overhead)
          std::function<int(int)> func2 = [](int i) { return i+4; };
          std::cout << "func2: " << func2(6) << '\n'; 
      }