31:避免默认捕获模式

  • 闭包:lambda所创建的运行期对象
  • 默认捕获可能导致引用悬挂
    • 默认传引用可能导致引用悬挂
      • 显式传引用也可能导致引用悬挂,但是可以更容易发现此处可能有引用悬挂
    • 默认传值捕获也可能导致引用悬挂
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    std::vector<std::function<bool(int)>> filters;
    class Widget{
    public:
    //    void addFilter() const{
    //        filters.emplace_back(
    //            [=](int value) {return value % divisor == 0;} 
    //        ); // 看似是传值捕获,不会有引用悬挂;但是lambda只能捕获作用域中的非静态局部变量,此处的divisor其实是this->divisor,容易产生引用悬挂
    //    }
        // 解决方法:使用一个局部变量复制成员变量,然后使用显式的值捕获
        void addFilter() const{
            int divisorCopy = divisor;
            filters.emplace_back(
                [divisorCopy] (int value) {return value % divisorCopy == 0;}
            );
        }
    private:
        int divisor;
    };
    
  • lambda只能捕获作用域中的非静态局部变量,无法捕获静态或全局变量
    • 捕获表示将值拷贝到闭包类中,而lambda中使用静态或全局变量,相当于是对外部的引用,因此此时lambda不是独立的
  • 参考

32:使用初始化捕获将对象移入闭包

  • C++14使用初始化捕获模式(也称广义lambda捕获)来实现移动捕获
1
2
3
4
5
struct Widget{
    bool isValid() const;
};
auto func = [pw = std::make_unique<Widget>()] // 左边是lambda闭包内成员名称,右边是初始化
            {return pw->isValid();}
  • C++11使用std::bind间接实现移动捕获
1
2
3
4
5
6
7
struct Widget{
    bool isValid() const;
};
auto func = std::bind(
    [] (const std::unique_ptr<Widget>& pw) {return pw->isValid();},
    std::make_unique<Widget>()
);

33:泛型lambda的完美转发版本

auto&&类型的形参使用decltype,以std::forward

  • 泛型lambda(C++14):可以使用auto声明形参(即闭包类中的operator()可以使用模板实现)
    1
    2
    3
    4
    5
    6
    7
    
    auto f = [] (auto x) {return func(x);}
    // 闭包类中的operator()的大致实现:auto形参实际上是模板类型推导
    class SomeCompilerGeneratedClassName{
    public:
        template <typename T>
        auto operator() (T x) const {return func(x);}
    }
    
  • 泛型lambda的完美转发版本:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    auto f = [] (auto&& param) {return func( std::forward<decltype(param)>(param) );} 
    // 闭包类中的operator()的大致实现
    class SomeCompilerGeneratedClassName{
    public:
        template <typename T>
        auto operator() (T&& param) const { return func( std::forward<decltype(param)>(param) ); }
    };
    
    auto fs = [] (auto&&... params) {return func( std::forward<decltype(params)>(params)... );} // 变长参数版本
    
  • 参考

34:优先选用lambda表达式,而非std::bind

对于C++11,除了个别边缘case,lambda比std::bind更有优势;C++14,lambda完全可以替代std::bind

  • lambda可读性更强,更容易理解
    • 使用std::bind需要保持参数位置,同时需要了解其实现机制
      • std::bind需要保持参数位置,因此使用时需要查看原来函数的声明,才能知道占位符对应的参数类型和参数含义;但是lambda形参列表很明确
      • std::bind默认将参数拷贝到绑定对象内部(可以使用std::ref指定传引用),但是lambda可以明确指出值捕获还是引用捕获
      • std::bind绑定对象的函数调用使用了完美转发机制,但是lambda可以从形参列表中清晰看出传值还是传引用
      1
      2
      3
      4
      5
      6
      7
      
      Widget w; 
      Logger logger;
      auto f = [w, &logger] (CompressLevel level) { return compress(w, level, logger); } 
      // 捕获对象:w值捕获,logger引用捕获;形参:level传值
      
      auto g = std::bind(compress, w, std::placeholders::_1, std::ref(logger)); // 需要对应参数顺序
      // 绑定对象:w值绑定(复制),logger引用绑定;形参:level使用完美转发机制
      
    • std::bind参数绑定和对象调用不是一个时间,因此可能出现逻辑错误(见参考)
  • lambda灵活性更强
    • 如果std::bind绑定的函数存在重载版本,则编译器无法确定使用哪个版本的重载函数
      1
      2
      3
      4
      5
      6
      7
      
      void func(int a);
      void func(int a, int b);
      
      auto f = [] (int b) { return func(0, b); }
      
      using funcType = void(int, int);
      auto bnd = std::bind(static_cast<funcType>(func), 0, std::placeholders::_1)
      
  • lambda可以内联
    • 因为std::bind中绑定的是函数指针,需要在运行时才能确定;但是lambda中包含函数体,可以进行内联
  • 使用std::bind的两个场景:在C++11中
    • 使用std::bind间接实现移动捕获([[ch06-lambda表达式#32:使用初始化捕获将对象移入闭包|C++14支持移动捕获]])
    • 使用std::bind绑定参数的完美转发机制,间接多态函数对象([[ch06-lambda表达式#33:泛型lambda的完美转发版本|C++14支持泛型lambda]])
      1
      2
      3
      4
      5
      6
      7
      8
      
      auto f = [callableObject] (const auto& param) { callableObject(param); };
      
      class CallableObject{
      public:
          template <typename T> 
          void operator() (const T& param);
      };
      auto g = std::bind(CallableObject(), std::placeholders::_1); // 将占位符参数完美转发到可调用对象的调用运算符中
      
  • 参考