天天看点

C++学习笔记-lambda表达式

  1. lambda表达式的值捕获或者引用捕获只能捕获lambda表达式的作用域内可见的非静态局部变量,包括形参;
  2. 在构造lambda表达式的时候,尽量不要使用默认的值捕获或者默认的引用捕获,而是直接将需要捕获的变量放到捕获列表中;
  3. 在类中使用lambda捕获成员变量时,无法通过将成员变量加入到捕获列表里面去捕获,可以通过默认的值捕获的方式捕获成员变量:
    class A
    {
    private:
    	int m;
    
    public:
    	void f()
    	{
    		auto r = [](){std::cout << m;}; // 错误,没有捕获变量m
    		auto r = [m](){std::cout << m;}; // 错误,无法将成员变量放到捕获列表中
    		auto r = [=](){std::cout << m;}; // 可以,但是不建议,全部变量通过值捕获,不过实际捕获的是this指针(得到this的副本),而不是变量m
    		auto r = [&](){std::cout << m;}; // 可以,但是不建议
    		auto r = [s = m](){std::cout << s;}; // 正确,C++14中的初始化捕获模式(又叫广义捕获),使用m初始化闭包类中的成员变量s
    		
               
  4. lambda中按引用捕获比较容易产生空悬引用(类似于空悬指针),按值捕获极易受空悬指针的影响,并且会误导人们认为此lambda表达式是自洽的(自洽是指与外部的环境是绝缘的,外部不管怎么变化,都不影响内部的状态);
  5. lambda表达式的初始化捕获与

    std::bind

    类似,都是在创建表达式(或者说创建闭包或绑定对象的时候),就进行了初始化,会在闭包内部创建一个内部对象,并对其进行初始化,而等到调用的时候,才将此内部对象作为实参传递给执行对象:
    std::vector<int> vi;
    auto f1 = [vi = std::move(vi)]() {std::cout << vi[0];}; // 1
    auto f2 = std::bind([](const std::vector<int>& v){ std::cout << v[0];}, std::move(vi)); // 2 bind返回的是一个绑定对象,也是可调用对象
    
               
    表达式1在创建的时候就已经将vi的值进行了移动操作,编译器会在闭包类内部创建一个成员变量vi,然后将外部的vi通过移动构造来初始化闭包类内的vi,而不是在调用的时候才执行。同样,表达式2在创建绑定对象的时候就已经将vi的值移动到了内部一个成员变量,然后等到真正调用f2的时候,才将内部的这个变量传递给形参v,所以这里的形参v并不是针对vi的,是针对内部变量的。此外,默认情况下lambda表达式闭包里的所有变量都是const的(因为其

    operator()

    的声明默认是const的,所以成员变量在const成员函数内部都会隐式加上const属性(通过this指针加上的)),而绑定对象里移动构造得到的vi副本并不带有const属性,所以为了防止vi副本本修改,所以其对应的lambda表达式的形参被声明为了const引用,这样表达式2的行为就会与表达式1一致了。如果lambda表达式声明的时候带有

    mutable

    ,则闭包里的

    operator()

    声明就不会加上const,因此lambda表达式内部的成员变量就未加上const修饰符了。此时如果想让表达式2与表达式1一致,则其形参也要去除const:
    std::vector<int> vi;
    auto f1 = [vi = std::move(vi)]() mutable {std::cout << vi[0];}; // 1
    auto f2 = std::bind([](std::vector<int>& v) mutable { std::cout << v[0];}, std::move(vi)); // 2
    
               
    所以说,mutable只是去除了lambda表达式生成的闭包中

    operator()

    成员函数的const属性,让闭包中的成员函数在

    operator()

    成员函数内部不再有const属性了,可以通过C++在线代码解析查看编译器展开后的实现;
  6. C++14中,使用初始化捕获(即广义捕获)可以对象移动到闭包内(而不是复制或者通过引用传递,是移动操作)。在C++11中,没有初始化捕获,可以通过

    std::bind

    将对象移动到绑定对象(由

    std::bind

    生成的一种可调用对象)中来模拟初始化捕获;
  7. 当x是一个万能引用时(即

    T&& x

    ),此时

    decltype(x)

    的类型要么是左值引用(当传入的实参是左值时),要么是一个右值引用(当传入的实参是一个右值时),不会是左值或者右值;
  8. 带有完美转发功能的lambda表达式的书写格式:
    // 单形参格式
    auto f = [](auto&& param)
    {
    	return func(std::forward<decltype(param)>(param)));
    }
    
    // 任意多形参格式
    auto f = [](auto&&... params)
    {
    	return func(std::forward<decltype(params)>(params)...));
    }
    
               
  9. lambda表达式可以完成

    std::bind

    的功能,且可读性更好,且更容易生成效率更高的代码。并且不容易出错。所以尽量使用lambda表达式而不是

    std::bind

  10. 使用

    std::bind

    传入形参的值是在调用

    std::bind

    的时候求值的,而不是在执行其返回的可调用对象的时候求值的,所以当传入的是当前时间相关的变量时候,就有可能出错;
  11. std::bind

    总是复制其实参到创建的绑定对象中,如果想传引用,则得使用

    std::ref

    对实参进行包装。不过,后面调用的时候,将

    std::bind

    中的那些副本参数传给指定函数是通过万能引用来传递的,也就是传递的是引用;
  12. std::bind

    在C++14及更新的版本中毫无用武之地(使用lambda代替它),在C++11中仅在两个场合还算有使用的理由:移动捕获、多态函数对象。