C++ lambda

lambda表达式是什么

        参考?:c++函数对象与lambda表达式实现原理_~怎么回事啊~的博客-CSDN博客

lambda避免默认捕获模式 

引用传递

#include <iostream>

int var_global = 9;


class Bar{
public:
    Bar(int val){
        var_dataMember = val;
    }

    auto func(int var_localParam){
        static int var_localStatic = 10;
        int var_local = 12;
        
        return [&](){
            std::cout <<" var_localParam : " << var_localParam << ";var_local :"
                << var_local << std::endl;
            std::cout <<" var_localStatic : " << var_localStatic << ";var_dataMember :"
                << var_dataMember <<  ";var_global : " << var_global << std::endl;
        };
    }

private:
    int var_dataMember;
};

int main(){
    Bar bar(8);
    auto f = bar.func(6);
    f();

    return 0;
}

将上述的代码复制到:C++ Insights

主要用于看代码编译展开后的具体情况,点击run

#include <iostream>

int var_global = 9;



class Bar
{
  
  public: 
  inline Bar(int val)
  {
    this->var_dataMember = val;
  }
  
  inline __lambda_16_16 func(int var_localParam)
  {
    static int var_localStatic = 10;
    int var_local = 12;
        
    class __lambda_16_16
    {
      public: 
      inline /*constexpr */ void operator()() const
      {
        std::operator<<(std::operator<<(std::cout, " var_localParam : ").operator<<(var_localParam), ";var_local :").operator<<(var_local).operator<<(std::endl);
        std::operator<<(std::operator<<(std::operator<<(std::cout, " var_localStatic : ").operator<<(var_localStatic), ";var_dataMember :").operator<<(__this->var_dataMember), ";var_global : ").operator<<(var_global).operator<<(std::endl);
      }
      
      private: 
      int & var_localParam;
      int & var_local;
      Bar * __this;
      
      public:
      __lambda_16_16(Bar * _this, int & _var_localParam, int & _var_local)
      : __this{_this}
      , var_localParam{_var_localParam}
      , var_local{_var_local}
      {}
      
    } __lambda_16_16{this, var_localParam, var_local};
    
    return __lambda_16_16;
  }
  
  
  private: 
  int var_dataMember;
  public: 
};



int main()
{
  Bar bar = Bar(8);
  __lambda_16_16 f = bar.func(6);
  f.operator()();
  return 0;
}

         lambda表达式本质上是一个函数对象,在这里是__lambda_16_16:

 上述的代码在gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0下运行结果:

ok@ok-Precision-3630-Tower:/data/test/c++$ ./testlambda 
 var_localParam : 32765;var_local :12
 var_localStatic : 10;var_dataMember :8;var_global : 9

由于int var_localParam形参在函数结束后被释放,var_localParam被引用捕捉,因此是垃圾值,函数栈上分配的var_local 由于编译器原因,还可以访问?

值传递捕获的是this指针

#include <iostream>


class Bar{
public:
    Bar(int val){
        divisor_ = val;
    }


    auto bindTest() {
        return [=] (int value)-> int {
             std::cout <<  "divisor_ :" <<  divisor_<< std::endl;
            return value - divisor_;
        };
    }
private:
    int divisor_;
};


auto getF() {
    Bar bar(8);
    auto f = bar.bindTest();
    return f;
}

int main(){

    auto f = getF();

    std::cout <<  f(10) << std::endl;
    return 0;
}

输出:

ok@ok-Precision-3630-Tower:/data/test/c++$ ./testlambda 
divisor_ :21897
-21887

在getF 调用结束后,Bar bar 类的实例化对象被释放,divisor_保存的数据成为垃圾值

解决this释放的问题:增加一个局部变量,拷贝成员变量的值并且值传递到lambda中:

C++14 初始化捕获

 无法在Lambda中捕获静态变量 

具有静态存储持续时间的变量不需要捕获,因此无法捕获。您可以在lambda中简单地使用它

#include <iostream>

auto bindF(){
    static int divisor = 10;
    divisor++;
    std::cout <<"divisor:" << divisor <<";&divisor:" << &divisor << std::endl;
    return [=](int value) -> int {
        std::cout <<"lambda divisor:" << divisor <<";&divisor:" << &divisor << std::endl;
        return value - divisor;
    };
}
int main(){
    auto f1 = bindF();
    std::cout <<  f1(10) << std::endl;

    auto f2 = bindF();
    std::cout <<  f2(10) << std::endl;   
    return 0;
}

输出:

ok@ok-Precision-3630-Tower:/data/test/c++$ ./testlambda 
divisor:11;&divisor:0x56072a4ba010
lambda divisor:11;&divisor:0x56072a4ba010
-1
divisor:12;&divisor:0x56072a4ba010
lambda divisor:12;&divisor:0x56072a4ba010
-2

C++14的Lambda新特性:对捕获变量的初始化

在C++14 中新增了对捕获变量的初始化,且不影响原来的变量

    int a;
    auto func = [a = 10]()mutable{
        cout << a << endl;
    };
    func();
    cout << "a:" << a << endl; //这里的a还是没有初始化的状态

        如果你有一个对象,其复制操作开销昂贵,而移动操作成本低廉,而你又需要把该对象放入闭包,那么你肯定更愿意移动该对象,而非复制它。

        C++14为我们提供了一个全新的捕获方式---初始化捕获,也叫做广义lambda捕获,使用初始化捕获,则你会得到机会指定:

由lambda生成的闭包类中的成员变量名字。

一个表达式,用以初始化该成员变量。

例如使用初始化捕获将std::unqiue_ptr移动到闭包内:

class Widget {                  //一些有用的型别
public:
    ...
 
    bool isValidted() const;
    bool isProcessed() const;
    bool isArchived() const;
private:
    ...
};
auto pw = std::make_unique<Widget>();   //创建Widget
                                        //关于std::make_unique,参见Item 21
 
...                                     //配置*pw
 
auto func = [pw = std::move(pw)]            //采用std::move(pw)
            { return pw->isValidated() &&   //初始化闭包类的数据成员
                    pw->isArchived();};


        [] 中的那段代码就是初始化捕获,位于“=”左边的,是你所指定的闭包类成员变量的名字,而位于其右侧是初始化表达式,且它们处于不同的作用域,左侧作用域就是闭包类的作用域,右侧的作用域与lambda式加以定义之处的作用域相同。

        pw=std::move(pw)表达了"在闭包中创建一个成员变量pw,然后使用针对局部变量pw实施std::move的结果来初始化该成员变量"。

        C++11显然并不支持这种初始化捕获,那该如何在C++11中实现移动捕获呢?

        在C++11中按移动捕获可以采用以下方法模拟,只需要:

        把需要捕获的对象移动到std::bind产生的函数对象中。
给到lambda式一个指涉到欲“捕获”的对象的引用。
例如,如果你想创建一个局部的std::vector对象,向其放入一组值,然后将其移动入闭包。在C++14使用初始化捕获,非常简单:

std::vector<double> data;       //欲移入闭包的对象
 
...                             //灌入数据
 
auto func = [data = std::move(data)]    //C++14的初始化捕获
            {/* 对数据加以运用 */}


但是在C++11中,你必须借助std::bind生成函数对象:

std::vector<double> data;       //同前
 
...                             //同前
 
auto func = std::bind(
            [](const std::vector<double>& data)    //C++11中模拟移动捕获的部分
            {/* 对数据加以运用 */},
            std::move(data)
        );


和lambda表达式类似地,std::bind也生成函数对象,第一个实参是个可调用对象,第二个实参表示传给该对象的值。

我们知道,std::bind对于每个左值实参,在绑定对象内的对应的对象内对其实施的是复制构造,而对每个右值实参,实施的则是移动构造。所以当一个绑定对象被“调用”,也就是当func(绑定对象)被调用时,func内经由移动构造出所得到的data的副本就会作为实参传递给那个原先传递给std::bind的lambda式。

至此,我们应该知道以下知识点:

        移动构造一个对象入C++闭包是不可能实现的,但移动构造一个对象入绑定对象则是可以实现的
        欲在C++11中模拟移动捕获,需要包含以下步骤:

  • 先移动构造一个对象入绑定对象
  • 然后按照引用把移动构造所得的对象传递给lambda表达式

因为绑定对象的生命周期和闭包相同,所以针对绑定对象中的对象和闭包内的对象可以采用相同手法处置

lambda表达式实现完美转发,对auto&&型别的形参使用decltype,以std::forward之 

        泛型lambda式(generic lambda)是C++14最振奋人心的特征之一,lambda可以在形参规格中使用auto。这个特性的实现十分直接了当:闭包类中的operator()采用模板实现。例如,给定下述lambda式:

auto f = [](auto x) { return func(normalize(x));};
以上语句翻译的闭包类代码如下:

class SomeComilerGeneratedClassName {
public:
    template<typename T>                //auto型别的返回值
    auto operator()(T x) const          //参见Item 3
    {  return func(normalize(x));}
    ...
};                                      //闭包类的其他功能


        在本例中,lambda式对x实施的唯一动作就是将其转发给normalize。但是如果normalize区别对待左值和右值,那么该lambda式撰写的是有问题的,因为在此lambda总会传递左值(形参x)给normalize,即使传递给lambda式的实参是个右值。

        该lambda式的正确撰写方式是把x完美转发给normalize,这就要求代码中修改两处,首先,x要改成万能引用(参见Item 24);其次,使用std::forward(参见Item 25)把x转发给normalize。概念上不难理解,这两处的修改都是举手之劳:

        auto f = [](auto&& x){ return func(normalize(std::forward<???>(x)));};


可是问题就出在,上面代码里面的???应该怎么办呢?

        通常情况下,在使用完美转发的时候,你是在一个接受型别形参T的模板函数中,所以,你写std::forward<T>就好。但是在泛型lambda式中,却没有可用的型别形参T。在lambda式生成的闭包内的模板化operator()函数中的确有个T,但是在lambda式中无法指涉,所以也没用。

        前面我们已经说过,如果把实参左值传递给万能引用型别的形参,则该形参的型别会被推导为左值引用,如果传递是右值,则该形参会成为右值引用。那这也就意味着我们可以在该lambda表达式中探查x的型别,判断传入的实参是左值还是右值。而decltype则可以完成探查工作,如果传入的是个左值,decltype(x)将会产生左值引用型别,如果传入的是个右值,decltype(x)将会产生右值引用。

        我们还知道,使用std::forward时惯例是:用型别形参为左值引用表明想要返回左值,而用非引用型别时来表达想要返回的右值。

        再看看我们的lambda式,如果x绑定了左值,decltype(x)将产生左值引用型别。这符合惯例。不过,如果x绑定的是个右值,decltype(x)将会产生右值引用惯例,并非惯例的右值。

        但是这里需要说明的是,在使用std::forward时,使用一个右值引用型别和使用一个非引用型别,会产生相同结果。所以如果x绑定的是个右值,decltype(x)后的std::forward()依然符合惯例。

示例代码如下:
 

//单一变量
auto f = 
    [](auto&& param)
    {
        return func(normalizer(std::forward<decltype(param)>(param)));
    }
//多变量
 
auto f = 
    [](auto&&... param)
    {
        return func(normalizer(std::forward<decltype(param)>(param)...));
    }

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

优势1:可读性更好


我们假设有个函数用来设置声音警报,示例代码:

// 表示时刻的型别typedef(语法参见Item 9)
using Time = std::chrono::steady_clock::time_point;
 
// 关于“enum class”,参见Item 10
enum class Sound {Beep, Siren, Whistle};
 
// 表示时长的型别typedef
using Duration = std::chrono::steady_clock::duration;
 
// 在时刻t,发出声音s,持续时长d
void setAlarm(Time t, Sound s, Duration d);


这里进一步假设,在程序的某处,我们想要设置在一小时之后,发出警报并持续30秒。警报的具体声音,却尚未确定。

这么一来,我们可以撰写一个lambda式,修改setAlarm的接口,这个新的接口只需要指定声音即可:

// setSoundL("L"表示Lambda)是个函数对象
// 它接受指定一个声音
// 该声音将在设定后一小时发出,并持续30秒
 
auto setSoundL = 
        [](Sound s)
        {
            //使std::chrono组件不加限定饰词即可使用
            using namespace std::chrono;
            setAlarm(steady_clock::now() + hours(1),    //报警发出的时刻为1小时后
                     s,
                     seconds(30));                      //持续30秒
        };

如果是在C++14里,那么上述的代码可以写的更具可读性:

auto setSoundL = 
        [](Sound s)
        {
            using namespace std::chrono;
            using namespace std::literals;          //C++14支持实现后缀
            setAlarm(steady_clock::now() + 1h,      //这里直接用1h表示
                     s,
                     30s);                          //30s表示
        };


那么如果用std::bind来写会是什么样呢?下面的代码其实包含了一个错误,我们后续会修复它,先看看代码:

using namespace std::chrono;
using namespace std::literals;
 
using namespace std::placeholders;      //这里是因为要用bind对应的占位符
 
auto setSoundB =                        //B表示bind
        std::bind(setAlarm, 
                  steady_clock::now() + 1h,    //这里有个错误!
                  _1,
                  30s);


对于初学者而言,这种“_1"占位符简直好比天书,但即使是行家,也许脑补出从阿占位符中数字到它在std::bind形参列表位置的映射关系,才能理解,在调用setSoundB时传入的第一个参数,会作为第二个实参传递给setAlarm。而且你还不知道这个实参的类型是什么,需要查看setAlarm的声明。

接下来看看错误在哪里:

在lambda式中,表达式steady_clock::now() + 1h是setAlarm的实参之一,这一点清清楚楚,该表达式会在setAlarm被调用的时刻评估求值。这样做合情合理,我们就是想要在setAlarm被调用后的一个小时之后启动报警。

在std::bind的调用中,steady_clock::now() + 1h作为实参被传递给了std::bind,而非setAlarm。意味着表达式评估求值的时刻是在调用std::bind的时刻,并且求得的时间结果会被存储在结果绑定对象中。最终导致的结果是,报警被设定的启动时刻是在调用std::bind的时刻之后的一个小时,而并非调用setAlarm的时刻之后的一个小时!
想解决这个问题,就像需要让std::bind来延迟表达式的评估求值到调用setAlarm的时候,而实现这一点的途径是在原来的std::bind上再嵌套一个std::bind.

//C++14,标准运算符模板的模板型别实参大多数情况可以不写
auto setSoundB = 
    std::bind(setAlarm,
              std::bind(std::plus<>(), steady_clock::now(), 1h),
              _1,
              30s);
//C++11
auto setSoundB = 
    std::bind(setAlarm,
                std::bind(std::plus<steady_clock::time_point>(),            
                          steady_clock::now(), 
                          hours(1)),
              _1,
              seconds(30));     

    
事情到这里,已经很明显的显示出lambda的优势了,可读性强了不少。

优势2:遇到重载也没事


一旦对setAlarm实施了重载,新的问题就会马上出现。

假如有个重载版本会接受第四个形参,用以指定报警的音量:

enum class Volume {Normal, Loud, LoudPlusPlus};
void setAlarm(Time t, Sound s, Duration d, Volume v);


之前的lambda表示没问题,可以正常调用3形参版本重载。

但是对std::bind的调用,就无法通过编译了:

auto setSoundB = 
    std::bind(setAlarm,     //错误!这里不知道如何选择了
                std::bind(std::plus<steady_clock::time_point>(),            
                          steady_clock::now(), 
                          hours(1)),
              _1,
              seconds(30));  


错误的根因在于编译器只拿到一个函数名,但是这个函数本身是多义的。

如果还是要让std::bind能运作,那么要写成这样:

using SetALarm3ParamType = void(*)(Time t, Sound s, Duration d);
 
auto setSoundB = 
    std::bind(static_cast<SetALarm3ParamType>(setAlarm),
                std::bind(std::plus<steady_clock::time_point>(),            
                          steady_clock::now(), 
                          hours(1)),
              _1,
              seconds(30));  


优势3:可能性更高的生成更快的代码


        但即便你觉得这样写好,多写几行没什么问题,但这么写还是带来了更大的问题:
在SetSoundL的函数中,调用setAlarm采用的是常规的函数唤起方式,这么一来,编译器就可以用惯常的手法将其内联:

setSoundL(Sound::Siren);        //这里,setAlarm的函数体大可以被内联


        可是,std::bind的调用传递了一个指涉到setAlarm的函数指针,而那就意味着setAlarm的调用是通过函数指针发生的。编译器不太会内联掉通过函数指针发起的函数调用,所以后一种写法被内联的几率大大降低。所以lambda式形式的调用被优化的可能性更高。

        而不仅如此,上面的例子仅仅涉及一个函数调用,如果你想做的事情比这更复杂,使用lambda式的好处会急剧增大。

看下面一个例子,需求是判断某个参数是否在最大值和最小值之间:

//C++14
auto betweenL = 
    [lowVal, highVal]
    (const auto& val)
    { return lowVal <= val && val <= highVal;};
//std::bind C++14
 
using namespace std::placeholders;
 
auto betweenB = 
    std::bind(std::logical_and<>(),
            std::bind(std::less_equal<>(), lowVal, _1),
            std::bind(std::less_equal<>(), _1, highVal));


C++11还不支持模板泛型自动推导,还要改成这样:

//std::bind C++11
 
using namespace std::placeholders;
 
auto betweenB = 
    std::bind(std::logical_and<bool>(),
            std::bind(std::less_equal<int>(), lowVal, _1),
            std::bind(std::less_equal<int>(), _1, highVal));

这么一对比,已经太明显不过了。

优势4:参数类型一目了然


试想这样一个场景,需要压缩一个类,然后返回这个类的副本:

enum class CompLevel { Low, Normal, High};
 
Widget compress(const Widget& w, CompLeve leve)


然后写了个函数对象包装一下:

Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);


但是这里w是按值传递的还是按引用传递的,就让人很迷惑了。(这里有个前提,std::bind默认就是按值传递的,如果要用按引用,要显示写成:

auto compressRateB = std::bind(compress, std::ref(w), _1);


而lambda就很明显是按值:

auto compressRateL = 
    [w](CompLeve lev)
    { return compress(w, lev);};
不仅仅是这里声明和定义的地方让人迷惑,调用的形式也让人不清不楚:

//Lambda式
compresssRateL(CompLevel::High);        //实参按值传递
//bind式
compresssRateB(CompLevel::High);        //这里是按照什么呢?


答案又会让你出乎意料,而且还是死记硬背没有原因的:

结论
C++14里已经可以忘记std::bind了。

C++11里还有两种受限场合可以使用:

移动捕获:C++11语言不支持初始化捕获,只能用bind来模拟
多态函数对象: 因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何型别的实参。这个特点再你想要绑定的对象具有一个函数调用运算符模板是,是有利用价值的。
例如:

class PolyWidget {
public:
    template<typename T>
    void operator()(const T& param);
    ...
};


这里来个bind:

PolyWidget pw;
 
auto boundPw = std::bind(pw, _1);
这样一来,boundPw就可以通过任意性别的实参加以调用:

boundPw(1995);
 
boundPw(nullptr);
 
boundPw("Adam Xiao");
在C++11中的lambda式是办不到的,因为不支持泛型,但是C++14里依旧可以做到

//C++14
 
auto boundPw = [pw](const auto& param)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值