C++11初探:lambda表达式和闭包
到了C++11最激动人心的特性了:
匿名函数:lambda表达式
假设你有一个vector<int> v, 想知道里面大于4的数有多少个。for循环谁都会写,但是STL提供了现成算法count_if,不用可惜。C++03时代,我们会这样写:
#include <iostream><vector><algorithm> gt4( x> ()( x><> v; 很多v.push_back(...);<<count_if(v.begin(),v.end(),gt4)<<<<count_if(v.begin(),v.end(),GT4())<<
就为这样一个微功能,要么写一个函数,要么写个仿函数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>
func( a, b) { a+<(,)> pfunc =<(,)> plambda = []( a, b){ 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的即视感有木有!
第二个:编译通过,运行输出是这个???
1167772160167772160167772160
看起来像是访问了无效内存。的确是这样。cur,step这两个局部变量在父函数的栈帧中,内部的匿名函数返回以后,父函数的栈帧就销毁了,而我们用的是“引用”,引用的变量已经没了。
既然放在栈上会有生存期问题,那就放堆里
auto Counter = [](int init,int step){ int* pcur = new int(init); int* pstep = new int(step); return [=](){ //注意!&变成了= *pcur += *pstep; return *pcur-*pstep; };};
注意使用了[=]而不是[&]。解释:
我们没有直接修改捕获的指针变量,而是修改它指向的变量,和[=]的规则不冲突
外部的指针还是在栈上,如果用[&]还是会引用到已销毁的指针。我们只需要复制一份指针值。
这样输出的确正常了,但是内存泄漏了。程序员的节操呢?
用智能指针可以解决内存泄漏:
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)~