zoukankan      html  css  js  c++  java
  • C++11初探:lambda表达式和闭包

    到了C++11最激动人心的特性了:

    匿名函数:lambda表达式

    假设你有一个vector<int> v, 想知道里面大于4的数有多少个。for循环谁都会写,但是STL提供了现成算法count_if,不用可惜。C++03时代,我们会这样写:

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    bool gt4(int x){
        return x>4;
    }
    
    struct GT4{
        bool operator()(int x){
            return x>4;
        }
    };
    
    int main(){
        vector<int> v;
       很多v.push_back(...);
       ... cout<<count_if(v.begin(),v.end(),gt4)<<endl; cout<<count_if(v.begin(),v.end(),GT4())<<endl; }

    就为这样一个微功能,要么写一个函数,要么写个仿函数functor,还不如手写循环简单,这是我的感受。如果用过其他语言的lambda表达式,这种写法完全是渣渣。

    C++引入的lambda表达式提供了一种临时定义匿名函数的方法,可以这样写:

    int res = count_if(v.begin(),v.end(),[](int x){ return x>4; });

    世界瞬间美好了。既然是匿名函数,函数名自然不用写了,连返回类型都不用写了~想用一个函数,用的时候再写,大大提高了algorithm里各种泛型算法的实用性。

    一般的lambda表达式语法是

    [捕获列表] (参数列表) -> 返回类型 {函数体}

    ->返回类型可以省略;如果是无参的,(参数列表)也可以省略,真是各种省。匿名函数是个lambda对象,和函数指针有区别,但一般不用关心它。如果你想把一个匿名函数赋给一个函数指针类似物以待后续使用,可以用auto

    auto func = [](int arg) { ... };

    但捕获列表是什么?接下来:

    闭包closure

    如果改主意了,要求>k的个数,k运行时指定,怎么办?你可能会写

    int k;
    cin>>k;
    int res = count_if(v.begin(),v.end(),[](int x){
        return x>k;
    }); //WRONG!

    但是编译器报错:

    error: variable 'k' cannot be implicitly captured in a lambda
          with no capture-default specified
            return x>k;

    匿名函数不能访问外部函数作用域的变量?太弱了!

    如果真是这样,实用性的确有限。lambda的捕获列表就是指定你要访问哪些外部变量,这里是k,于是

    int res = count_if(v.begin(),v.end(),[k](int x){ //注意[]里的
         return x>k;
    }); //OK!

    如果要捕获多个变量,可以用逗号隔开。如果要捕获很多变量,干脆一起打包算了,用'='捕获所有:

    int res = count_if(v.begin(),v.end(),[=](int x){
         return x>k;
    }); //OK, too!

    通俗的说:子函数可以使用父函数中的局部变量,这种行为就叫做闭包。

    解释一下各种捕获方式:

    捕获capture有些类似传参。使用[k], [=]声明的捕获方式是复制copy,类似传值。区别在于,函数参数传值时,对参数的修改不影响外部变量,但copy的捕获直接禁止你去修改。如果想修改,可以使用引用方式捕获,语法是[&k]或[&]。引用和复制可以混用,如

    int i,j;
    [=i, &j] (){...};

    但闭包的能力远不止“使用外部变量”这么简单,最奇幻的是它可以超越传统C++对变量作用域生存期的限制。我们尝试一些刺激的。

    假设你要写一个等差数列生成器,初值公差运行时指定,行为和函数类似,第k次调用生成第k个值,并且各个生成器互不干扰,怎么写?

    普通函数不好优雅地保存状态(全局变量无力了吧)。用仿函数好了,成员变量保存每个计数器的状态:

    struct Counter
    {
        int cur;
        int step;
        Counter(int _init,int _step){
            cur = _init;
            step = _step;
        }
        int operator()(){
            cur = cur+step;
            return cur-step;
        }
    };
    int main(){
        Counter c(1,3);
        for(int i=0;i<4;++i){
            cout<<c()<<endl;
        } //输出1 4 7 10 
    }

    但是我们现在有了闭包!把状态作为父函数中的局部变量,各个counter就可以不影响了。由于要修改外部变量,根据之前的介绍,声明成引用捕获[&]。写起来大体像这样:

    ??? Counter(int init,int step){
        int cur = init;
        return [&]{
            cur += step;
            return cur-step;
        }
    }
    int main(){
        auto c = Counter(1,3);
        for(int i=0;i<4;++i){
            cout<<c()<<endl;
        }
    }

    两个问题!

    第一个:Counter函数的返回类型怎么写???

    Counter返回值是一个lambda,赋给c时可以用auto骗过去,但声明时写类型是躲不过去了。返回类型后置+decltype救不了你,因为后置了decltype还是获取不到返回值类型。lambda对象,虽然行为像函数指针,但是不能直接赋给一个函数指针。

    介绍一个C++11新的模板类function,是消灭丑陋函数指针的大杀器。你可以把一个函数指针或lambda赋给它,例如

    
    
    #include <functional>
    int func(float a,float b) {
    return a+b;
    } function
    <int(float,float)> pfunc = func; function<int(float,float)> plambda = [](float a,float b){ return a+b;};

    比函数指针好看多了。

    于是这里可以写:

    function<int()> Counter(int init,int step){ ... }

    但是!如果再疯狂一点,匿名函数可以省略返回类型,auto可以推导类型,结合起来这样写是可以的!

    auto Counter = [](int init,int step){
        int cur = init;
        return [&](){
            cur += step;
            return cur-step;
        };
    }; //不要漏';' 根本上还是赋值语句

    “类型推导, auto和decltype”一节里留的trick就是这个。javascript的即视感有木有!

    第二个:编译通过,运行输出是这个???

    1
    167772160
    167772160
    167772160

    看起来像是访问了无效内存。的确是这样。cur,step这两个局部变量在父函数的栈帧中,内部的匿名函数返回以后,父函数的栈帧就销毁了,而我们用的是“引用”,引用的变量已经没了。

    既然放在栈上会有生存期问题,那就放堆里

    auto Counter = [](int init,int step){
        int* pcur = new int(init);
        int* pstep = new int(step);
        return [=](){ //注意!&变成了=
            *pcur += *pstep;
            return *pcur-*pstep;
        };
    };

    注意使用了[=]而不是[&]。解释:

    1. 我们没有直接修改捕获的指针变量,而是修改它指向的变量,和[=]的规则不冲突
    2. 外部的指针还是在栈上,如果用[&]还是会引用到已销毁的指针。我们只需要复制一份指针值。

    这样输出的确正常了,但是内存泄漏了。程序员的节操呢?

    用智能指针可以解决内存泄漏:

    auto Counter = [](int init,int step){
        shared_ptr<int> pcur(new int(init));
        shared_ptr<int> pstep(new int(step));
        return [=](){
            *pcur += *pstep;
            return *pcur-*pstep;
        };
    };

    虽然解决了问题,但过于繁琐了。本质上,我们需要的效果是把父函数的局部变量生存期延长,至少和子函数一样长。C++11提供了mutable关键字,可以模拟这一功能:

    auto Counter = [](int init,int step){
        int cur = init;
        return [=] () mutable {
            cur += step;
            return cur-step;
        };
    };

    加上mutable,就告诉编译器,这个变量是父子函数共享的,子函数对它的修改要反映到外部,并且它的生存期要和子函数一样长!

    这里可能有点绕,函数哪来的生存期?注意这里“子函数”并不是真正的函数,只是一个lambda类型的变量,只是有函数的行为,一样有生存期。

     闭包最大的用处在于写回调函数,比如事件响应。当初学Java的时候,Swing里用户界面各种内部类,感觉很烦。现在Java终于也有闭包了(Java8)~

    目录

  • 相关阅读:
    文件的基本操作(1)
    ajax实现分页和分页查询
    ajax 实现三级联动下拉菜单
    ajax 实现加载页面、删除、查看详细信息,以及bootstrap网页的美化
    ajax 实现页面加载和内容的删除
    ajax 写登录
    jquery 的基础知识,以及和Javascript的区别
    PHP 实现简单购物车功能(2)
    php 实现简单加入购物车
    《zw版·Halcon-delphi系列原创教程》 Halcon分类函数014,tuple,元组
  • 原文地址:https://www.cnblogs.com/npbool/p/3434757.html
Copyright © 2011-2022 走看看