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;    };};

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

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

(0)

相关推荐

  • 带你了解C#每个版本新特性

    上学时学习C#和.NET,当时网上的资源不像现在这样丰富,所以去电脑城买了张盗版的VS2005的光盘,安装时才发现是VS2003,当时有一种被坑的感觉,但也正是如此,让我有了一个完整的.NET的学习生 ...

  • 他来了,他来了,C 17新特性精华都在这了

    程序喵之前已经介绍过C++11的新特性和C++14的新特性(点击对应文字,直接访问),今天向亲爱的读者们介绍下C++17的新特性,现在基本上各个编译器对C++17都已经提供完备的支持,建议大家编程中尝 ...

  • Lambda表达式

    记录 Lambda. 两种显示形式: // 第一种:表达式Lambda,右边主体为表达式. (parameters) => expression // 第二种:语句Lambda,右边主体为语句块 ...

  • 理解lambda表达式,为什么用它?

    初学者接触 lambda表达式 ,觉得语法很清奇,正好今天看到 python weekly 推送了一个youtube的视频教程,今天大邓就顺便领大家简单认识一下lambda语法. up主:PyLeni ...

  • 全栈Python 编程必备

    据说:2019年, 浙江信息技术高考可以考python了:2018年, Python 进入了小学生的教材:2018年, 全国计算机等级考试,可以考python 了:据外媒报道,微软正考虑添加 Pyth ...

  • 函数与Lambda表达式

    函数参数定义 # 学习人员:贾其豪# 开发时间:2021/1/31 14:05#函数定义默认值参数#函数定义时,给形参设置默认值,只有与默认值不符的时候才需要传递实参def fun(a,b=10): ...

  • 说说Python中的lambda表达式?

    公众号新增加了一个栏目,就是每天给大家解答一道Python常见的面试题,反正每天不贪多,一天一题,正好合适,只希望这个面试栏目,给那些正在准备面试的同学,提供一点点帮助! 小猿会从最基础的面试题开始, ...

  • Java函数式编程和lambda表达式

    为什么要使用函数式编程# 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基 ...

  • 深入浅出 Java 8 Lambda 表达式

    摘要:此篇文章主要介绍 Java8 Lambda 表达式产生的背景和用法,以及 Lambda 表达式与匿名类的不同等.本文系 OneAPM 工程师编译整理. Java 是一流的面向对象语言,除了部分简 ...

  • C++之Lambda表达式

    C++之Lambda表达式

  • fun = [lambda x: x*i for i in range(4)] 本质解析/原理,LEGB规则 闭包原理

    命名空间,闭包原理,参考点击本文 一.问题描述 fun = [lambda x: x*i for i in range(4)] for item in fun: print(item(1)) 上述式子 ...

  • C系列1-1:初探C

    https://m.toutiao.com/is/Jo541EK/ 一:什么是C++ 谈到C++,我们一定会联想到C语言,的确C++与C语言有着密不可分的关系. 编程语言分为两类--面向过程和面向对象 ...