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)~

目录

原文地址:https://www.cnblogs.com/npbool/p/3434757.html