1.简介
定义:
C++11新增了很多特性,lambda表达式(lambda expression)就是其中之一,很多语言都提供了 lambda 表达式,如 Python,Java ,C#等。本质上, lambda 表达式就是一个可调用的代码单元
关于闭包的理解,请参见web前端开发初学者十问集锦(4)。
作用:
以往C++需要传入一个函数的时候,必须事先进行声明,视情况可以声明为一个普通函数然后传入函数指针,或者声明一个仿函数(functor,函数对象),然后传入一个对象。比如C++的STL中很多算法函数模板需要传入谓词(predicate)来作为判断条件,如排序算法sort。谓词就是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unary predicate,只接受单一参数)和二元谓词(binary predicate,接受两个参数)。接受谓词的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。如下面使用sort()传入比较函数shorter()(这里的比较函数shorter()就是谓词)将字符串按长度由短至长排列。
//谓词:比较函数,用来按长度排列字符串
bool shorter(const string& s1,const string& s2){
return s1.size()<s2.size();
}
//按长度由短至长排列words
std::sort(words.begin(),words.end(),shorter);
lambda表达式可以像函数指针、仿函数一样,作为一个可调用对象(callable object)被使用,比如作为谓词传入标准库算法。
也许有人会问,有了函数指针、函数对象为何还要引入lambda呢?函数对象能维护状态,但语法开销大,而函数指针语法开销小,却没法保存函数体内的状态。如果你觉得鱼和熊掌不可兼得,那你可错了。lambda函数结合了两者的优点,让你写出优雅简洁的代码。
语法格式:
lambda 表达式就是一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可以定义在函数内部,其语法格式如下:
[capture list](parameter list) mutable(可选) 异常属性->return type{function body}
capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表,通常为空,表示lambda不使用它所在函数中的任何局部变量。parameter list(参数列表)、return type(返回类型)、function body(函数体)与任何普通函数基本一致,但是lambda的参数列表不能有默认参数,且必须使用尾置返回类型。 mutable表示lambda能够修改捕获的变量,省略了mutable,则不能修改。异常属性则指定lambda可能会抛出的异常类型。
其中lambda表达式必须的部分只有capture list和function body。在lambda忽略参数列表时表示指定一个空参数列表,忽略返回类型时,lambda可根据函数体中的代码推断出返回类型。例如:
auto f=[]{return 42;}
我们定义了一个可调用对象f,它不接受任何参数,返回42。auto关键字实际会将 lambda 表达式转换成一种类似于std::function的内部类型(但并不是std::function类型,虽然与std::function“兼容”)。所以,我们也可以这么写:
std::function<int()> lambda = [] () -> int { return val * 100; };
如果你对std::function<int()>
这种写法感到很神奇,可以查看 C++ 11 的有关std::function的用法。简单来说,std::function<int()>
就是一个可调用对象模板类,代表一个可调用对象,接受 0 个参数,返回值是int。所以,当我们需要一个接受一个double作为参数,返回int的对象时,就可以写作:std::function<int(double)>
调用方式:
lambda的调用方式与普通函数的调用方式相同,上面的lambda示例调用如下:
cout<<f()<<endl; // 打印42
//或者直接调用
cout<<[]{return 42;}()<<endl;
我们还可以定义一个单参数的lambda,实现上面字符串排序的shorter()比较函数的功能:
auto f=[](cosnt string& a,const string& b){
return a.size()<b.size();
}
//将lambda传入排序算法sort中
sort(words.begin(),word2.end(),[](cosnt string& a,const string& b){
return a.size()<b.size();
});
//或者
sort(words.begin(),word2.end(),f);
2.lambda的捕获列表
lambda可以获取(捕获)它所在作用域中的变量值,由捕获列表(capture list)指定在lambda 表达式的代码内可使用的外部变量。比如虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些在捕获列表中明确指明的变量。lambda在捕获所需的外部变量有两种方式:引用和值。我们可以在捕获列表中设置各变量的捕获方式。如果没有设置捕获列表,lambda默认不能捕获任何的变量。捕获方式具体有如下几种:
[] 不截取任何变量 [&} 截取外部作用域中所有变量,并作为引用在函数体中使用 [=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用 [=,&valist] 截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对以逗号分隔valist使用引用 [&,valist] 以引用的方式捕获外部作用域中所有变量,对以逗号分隔的变量列表valist使用值的方式捕获 [valist] 对以逗号分隔的变量列表valist使用值的方式捕获 [&valist] 对以逗号分隔的变量列表valist使用引用的方式捕获 [this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。
在[]中设置捕获列表,就可以在lambda中使用变量a了,这里使用按值(=, by value)捕获。
#include <iostream>
int main(){
int a = 123;
auto lambda = [=]()->void{
std::cout << "In lambda: " << a << std::endl;
};
lambda();
return 0;
}
编译运行:
$ g++ main.cpp -std=c++11
$ ./a.out
In lambda: 123
可变类型(mutable):
按值传递到lambda中的变量,默认是不可变的(immutable),如果需要在lambda中进行修改的话,需要在形参列表后添加mutable关键字(按值传递无法改变lambda外变量的值)。
#include <iostream>
int main(){
int a = 123;
std::cout << a << std::endl;
auto lambda = [=]() mutable ->void{
a = 234;
std::cout << "In lambda: " << a << std::endl;
};
lambda();
std::cout << a << std::endl;
return 0;
}
编译运行结果为:
$ g++ main.cpp -std=c++11
lishan:c_study apple$ ./a.out
123
In lambda: 234 //可以修改
123 //注意这里的值,并没有改变
如果没有添加mutable,则编译出错:
$ g++ main.cpp -std=c++11
main.cpp:9:5: error: cannot assign to a variable captured by copy in a non-mutable lambda
a = 234;
~ ^
1 error generated.
看到这,不禁要问,这魔法般的变量捕获是怎么实现的呢?原来,lambda是通过创建个类来实现的。这个类重载了操作符(),一个lambda函数是该类的一个实例。当该类被构造时,周围的变量就传递给构造函数并以成员变量保存起来,看起来跟函数对象(仿函数)很相似,但是C++11标准建议使用lambda表达式,而不是函数对象,lambda表达式更加轻量高效,易于使用和理解
3.lambda的常见用法
(1)lambda函数和STL
lambda函数的引入为STL的使用提供了极大的方便。比如下面这个例子,当你想遍历一个vector的时候,原来你得这么写:
vector<int> v;
v.push_back( 1 );
v.push_back( 2 );
//...
for ( auto itr = v.begin(), end = v.end(); itr != end; itr++ ){
cout << *itr;
}
现在有了lambda函数你就可以这么写:
vector<int> v;
v.push_back( 1 );
v.push_back( 2 );
//...
for_each(v.begin(),v.end(),[](int val){
cout << val;
});
而且这么写了之后执行效率反而提高了。因为编译器有可能使用”循环展开“来加速执行过程。
参考文献
[1]Stanley B. Lippman著,王刚 杨巨峰译.C++ Primer中文版第五版.2013:346-346
[2]C++教程之lambda表达式一
[3]C++11 新特性:Lambda 表达式
[4] 初窥c++11:lambda函数及其用法