lambda表达式

lambda表达式常用于创建闭包并将其用作传递给函数的实参,闭包可以复制,即对应于单独一个lambda式的闭包型别可以有多个闭包,例如:

{
	int nPosX;
	
	auto c1 = [nPosX](int nPosY){return nPosX*nPosY;};
	
	auto c2 = c1;
}

闭包是lambda表达式创建的运行期对象,实例化的闭包会产生闭包类,每个lambda式都会触发编译器生成一个独一无二的闭包类,而闭包中的语句会变成它的闭包类成员函数的可执行指令。lambda表达式和闭包类存在 于编译期,闭包存在于运行期

根据不同的捕获模式,闭包会持有数据的副本或者引用,正因为如此,在创建闭包时有以下注意事项

闭包注意事项


关于捕获我们先拿出一个规定:捕获只能针对于在创建lambda式的作用域内可见的非静态局部变量(包括形参)

  • 以引用的方式捕获非静态局部变量

    当试图以引用的方式捕获非静态局部变量时,要注意局部变量的生命周期,避免出现空悬引用,例如:

      using FilterContainer = std::vector<std::function<bool(int)>>;
      FilterContainer filters;
    	
      void addFilter()
      {
          auto nDivisor = ComputeDivisor();
          filters.emplace_back([&](int nValue){return nValue % nDivisor == 0;);
      }
    

    变量nDivisor随着函数addFilter结束而消亡了,那么添加到容器内的函数对变量nDivisor的引用会悬空,这个例子以隐式引用的方式对外部非静态局部变量捕获,显示引用也同样有空悬问题:

      void addFilter()
      {
          auto nDivisor = ComputeDivisor();
          filters.emplace_back([&nDivisor](int nValue){return nValue % nDivisor == 0;});
      }
    

    显示地列出lambda表达式所依赖的局部变量或形参是很好的编程习惯

    如果闭包会立即被使用就不会存在空悬风险,即对引用变量的使用在引用变量消亡之前,例如:

      bool IsMultiple()
      {
          auto nDivisor = ComputeDivisor();
    		
          using std::begin;
          using std::end;
    		
          return std::all_of(begin(filters),end(filters),[&nDivisor](const auto& nValue){return nValue % nDivisor == 0;});
      }
    

    这个例子中形参使用了auto类型推导,这项能力只用C++14版本以后才支持。解决引用空悬问题还可以使用按值捕获,但是按值捕获也并不安全,只是在这个例子中安全,例如:

      void addFilter()
      {
          auto nDivisor = ComputeDivisor();
          filters.emplace_back([=](int nValue){return nValue % nDivisor == 0;});
      }
    
  • 以值的方式捕获非静态局部变量

    按值捕获也并不全是安全的,例如:

      class CWidget
      {
      public:
          void addFilter()const;
    	
      private:
          int m_nDivisor;
      };
    	
      void CWidget::addFilter()const
      {
          filters.emplace_back([=](int nValue){return nValue % m_nDivisor == 0;});
      }
    

    这个例子中lambda表达式对类成员变量m_nDivisor进行按值捕获,上面提到关于捕获的规定是:捕获只能针对于在创建lambda式的作用域内可见的非静态局部变量(包括形参),而类成员变量并不 符合这个规定,但是能够编译成功,编译成功是因为编译器帮助我们添加一些必要代码:

      void CWidget::addFilter()const
      {
          auto objPtr = this;
          filters.emplace_back([=](int nValue){return nValue % objPtr->m_nDivisor == 0;});
      }
    

    顺便提一下,在类中的成员函数内使用其成员变量编译器内部会在成员变量前增加this->,这个例子中lambda表达式里使用的是this指针的副本,即捕获了this指针而不是成员变量m_nDivisor, 风险也就在这里,当类对象被释放时lambda表达式里的指针变成空悬指针,规避这个风险应该这样编写:

      void CWidget::addFilter()const
      {
          auto nDivisor = m_nDivisor;
          filters.emplace_back([=](int nValue){return nValue % nDivisor == 0;});
      }
    

    更好的编码习惯:

      void CWidget::addFilter()const
      {
          auto nDivisor = m_nDivisor;
          filters.emplace_back([nDivisor](int nValue){return nValue % nDivisor == 0;});
      }
    

    可能会写出这样的代码:

      void CWidget::addFilter()const
      {
          filters.emplace_back([m_nDivisor](int nValue){return nValue % m_nDivisor == 0;});
      }
    

    这就触犯了上面提到的规定,m_nDivisor并不是非静态局部变量,编译失败

    如果使用的是C++14,捕获成员变量更好的方法是使用广义lambda捕获,像这样:

      void CWidget::addFilter()const
      {
          filters.emplace_back([nDivisor = m_nDivisor](int nValue){return nValue % nDivisor == 0;});
      }
    

    规定里提到非静态局部变量,如果一个lambda表达式采用按值捕获,在表达式内部使用静态变量会怎么样呢?例如:

      void addFilter()
      {
          static auto nDivisor = ComputeDivisor();
    		
          filters.emplace_back([=](int nValue){return nValue % nDivisor == 0;});
    		
          ++nDivisor;
      }
    

    这段代码编译成功,只不过lambda表达式并不能捕获nDivisor,每次调用函数addFilter时添加到filters的每个lambda表达式的行为都不一样,nDivisor对应着新值, 实际效果就是按引用捕获nDivisor

上面列出了这两种捕获的注意事项,只有掌握了lambda表达式的注意事项,那么在实际编码中才能运用自如

初始化捕获


初始化捕获是C++14才有的特性,例如上面的例子:

void CWidget::addFilter()const
{
	filters.emplace_back([nDivisor = m_nDivisor](int nValue){return nValue % nDivisor == 0;});
}

“=”左右侧处于不同的作用域,左侧作用域是闭包类的作用域,右侧的作用域与lambda表达式定义处的作用域相同,使用初始化捕获为我们提供了两个机会:

  1. 可以指定由lambda生成的闭包类中的成员变量名字
  2. 可以用一个表达式初始化该成员变量

就像这个例子中,我们指定了闭包类成员变量的名字nDivisor,下面这个例子展示了用一个表达式初始化该成员变量:

void addFilter()
{
	filters.emplace_back([nDivisor = ComputeDivisor()](int nValue){return nValue % nDivisor == 0;});
}

这两个例子对成员变量的赋值采用的都是复制的形式,如果可以更期望以移动的方式初始化成员变量,以此提高效率,例如:

{
	auto pObj = std::make_unique<CWidget>();
	
	auto func = [pObj = std::move(pObj)](){...};
	
	// 亦或
	auto func = [pObj = std::make_unique<CWidget>()](){...};
}

如果想在C++11中实现上面的效果也可以,主要的效果就是以移动的方式捕获,由于C++11的限制以移动构造的方式捕获变量是不可能的,但是可以用其他手法实现, 只不过要编写很多代码,两种实现方式,一种不采用lambda表达式,一种采用lambda表达式

  • 不采用lambda表达式

    对于自定义的实现要支持可调用性,移动初始化成员变量

      class SelfClosure
      {
      public:
    	
          using DataType = std::unique_ptr<CWidget>;
          explicit SelfClosure(DataType&& ptr):m_ptr(std::move(ptr))
          {
          }
    	
          bool operator()()const 
          {
              ...
    			
              return true;
          }
    	
      private:
          DataType m_ptr;
      };
    	
      {
          auto fun = SelfClosure(std::make_unique<CWidget>());
      }
    
  • 采用lambda表达式

    这里要借助std::bind,例如下面的代码:

      {
          std::vector<double> data;
          ...
          auto fun = std::bind([](const std::vector<double>& data{},std::move(data)));
      }
    

    std::bind的第一个实参是个可调用对象,后面的实参表示传给std::bind返回的函数对象,将我们要捕获的对象移动到std::bind产生的函数对象, 在这个函数对象内部对右值实参std::move(data)采用的是移动构造,当fun被调用时,fun内经由移动构造所得的data副本就会作为实参传递给std::bind函数的第一个实参lambda表达式副本, 也就是说lambda表达式的形参是对经由移动构造所得的data副本的左值引用

    这个例子中闭包的生命周期和std::bind返回的函数对象生命周期相同

    总结下来,这种方式的实现就两步:

    1. 把需要捕获的对象移动到st::bind产生的函数对象中
    2. lambda表达式的形参以左值引用的方式指向欲捕获的对象

在C++14中,初始化捕获是将对象移入闭包,在C++11中经由手动实现的类或借助std::bind去模拟初始化捕获

对lambda表达式的形参类型使用auto&&


C++14的一个重要特性就是lambda表达式的形参类型可以编写为auto&&,例如:

auto fun = [](auto x){return TestFun(computeFun(x));};

从这个例子看出是对形参的转发,根据这篇文章的介绍,lambda表达式的形参应该使用通用引用auto&&、完美转发,像这样:

auto func = [](auto&& x){return TestFun(computeFun(std::forward<decltype(x)>(x)));};

这里使用了decltype对形参x类型推导,这里需要注意的点是如果x绑定了左值,那么decltype将产生左值引用,而如果x绑定的是右值,decltype(x)将产生右值引用,当一个右值引用作为 std::forward的类型时,将发生引用折叠,关于引用折叠可以参考这篇文章,引用折叠后的还是右值引用

相对bind函数lambda表达式更有优势


在C++11中std::bind有两个更实用的场景,到C++14这两个场景都可以实用lambda表达式替代,这两个场景是:

  1. 移动捕获

    上面已经提到了,使用std::bind来实现C++11的移动捕获

  2. 多态函数对象

    例如:

     class CPolyWidget
     {
     public:
    		
         template<typename T>
         void operator()(const T& param);
     }
    	
     {
         CPolyWidget obj;
         auto bound = std::bind(obj,std::placeholders::_1);
    		
         bound(2020);
         bound("bound");
     }  在C++11中lambda表达式无法达到这样效果,C++14很容易实现:
    
     {
         auto bound = [obj](const auto& param)
         {
             pw(param);
         };
     }
    

除了这两个场景,lambda表达式更有优势,下面列了几个优势

  • 可读性

    有了上面关于lambda表达式的介绍,下面的例子使用lambda表达式的可读性更高:

      enum class Sound {Beep,Siren,Whistle};
      using Time = std::chrono::steady_clock::time_point;
      using Duration = std::chrono::steady_clock::duration;
    	
      void setAlarm(Time t,Sound s,Duration d)
      {
          std::cout << "setAlarm(Time t,Sound s,Duration d)"<< std::endl;
      }
    	
      // 使用bind
      {
          using namespace std::chrono;
          using namespace std::literals;
          using namespace std::placeholders;
          auto setAlarmWithBind = std::bind(setAlarm, std::bind(std::plus<>(), std::chrono::steady_clock::now(),1h),_1,30s);
    
          setAlarmWithBind(Sound::Beep);
      }
    
      // 使用auto表达式
      {
          auto setAlarmWithLambda = [](Sound s) 
          {
              using namespace std::chrono;
              using namespace std::literals;
    
              setAlarm(std::chrono::steady_clock::now() + 1h, s, 30s);
          };
    
          setAlarmWithLambda(Sound::Whistle);
      }
    

    如果直观看不出来,针对这个例子提出一个问题,lambda表达是的形参很容易看出是按值传递,那么std::bind的实参是以什么形式传递的,std::bind函数返回的绑定对象的实参以什么方式传递的

    std::bind函数返回的绑定对象的实参是按引用传递的

  • 表达力

    还是借用这个例子,当函数setAlarm是个重载函数,这个例子就需要修改,lambda表达式不修改,bind需要修改

      enum class Volume {Normal,Loud};
      // 增加的重载函数
       void setAlarm(Time t,Sound s,Duration d,Volume v)
       {
          std::cout << "setAlarm(Time t,Sound s,Duration d,Volume v)"<< std::endl;
       }
    	
      // 使用bind
      {
          using namespace std::chrono;
          using namespace std::literals;
          using namespace std::placeholders;
          auto setAlarmWithBind = std::bind(setAlarm, std::bind(std::plus<>(), std::chrono::steady_clock::now(),1h),_1,30s);
    		
          setAlarmWithBind(Sound::Beep);
      }
    

    此时编译失败,原因是编译器不知道将哪个函数setAlarm传给std::bind函数,所以只能修改为:

      {
          using SetAlarmFun3Type = void(*)(Time t,Sound s,Duration d);
    		
          auto setAlarmWithBind = std::bind(static_cast<SetAlarmFun3Type>(setAlarm), std::bind(std::plus<>(), std::chrono::steady_clock::now(),1h),_1,30s);
      }
    
  • 运行效率

    借用重载函数setAlarm的例子,实际上传递给std::bind函数的是个函数指针,对setAlarm的调用是通过函数指针发生的,编译器并不会内联掉通过函数指针发起的函数调用,而lambda表达式 内调用函数setAlarm很有可能被内联掉,所以从运行效率上来看,lambda表达式可能生成比使用st::bind更快的代码