[转]C++11 lambda表达式
- 作者:小白将
- 链接:https://www.jianshu.com/p/d686ad9de817
lambda表达式是C++11中引入的一项新技术,利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。但是从本质上来讲,lambda表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现。但是它简便的语法却给C++带来了深远的影响。如果从广义上说,lamdba表达式产生的是函数对象。在类中,可以重载函数调用运算符(),此时类的对象可以将具有类似函数的行为,我们称这些对象为函数对象(Function Object)或者仿函数(Functor)。相比lambda表达式,函数对象有自己独特的优势。下面我们开始具体讲解这两项黑科技。
lambda表达式
先从一个简单的例子开始,我们定义一个输出字符串的lambda表达式,如下所示,表达式一般都是从方括号[]开始,然后结束于花括号{}:
1 | auto basic_lambda = []{cout<<"Hello Lambda"<<endl;}; //定义简单的lambda表达式 |
下面分别是包含参数和返回类型的lambda表达式:1
2auto add = [] (int a, int b)->int { return a+b;}; //返回类型需要用`->`符号指出
auto multiply = [](int a, int b) {return a*b;} //一般可以省略返回类型,通过自动推断就能得到返回类型
lambda表达式最前面的方括号提供了“闭包”功能。每当定义一个lambda表达式以后,编译器会自动生成一个 匿名类 ,并且这个类重载了()运算符,我们将其称之为闭包类型(closure type)。在运行时,这个lambda表达式会返回一个匿名的闭包实例,并且该实例是一个右值。闭包的一个强大之处在于其可以通过传值或引用的方式捕捉其封装作用域内的变量,lambda表达式前面的方括号就是用来定义捕捉模式以及变量的lambda捕捉块,如下所示:
1 | int main() |
当lambda捕捉块为空时,表示没有捕捉任何变量。对于传值方式捕捉的变量x,lambda表达式会在生成的匿名类中添加一个非静态的数据成员,由于闭包类重载()运算符是使用了const属性,所以不能在lambda表达式中修改传值方式捕捉的变量,但是如果把lambda标记为mutable,则可以改变(但是这里的改变只会对 lambda 表达式内部的代码有影响, 对外部不起作用),如下所示:
1 | int x = 10; |
而对于引用方式捕捉的变量,无论是否标记为mutable,都可以对变量进行修改,并且修改的值会影响到外部, 至于会不会在匿名类中创建数据成员,需要看不同编译器的具体实现。
lambda表达式只能作为右值,也就是说,它是不能被赋值的1
2
3
4
5auto a=[]{ cout<<"A"<<endl; };
auto b=[]{ cout<<"B"<<endl; };
a = b; // 非法,lambda表达式变量只能做右值
auto c = a; // 合法,生成一个副本
造成以上原因是因为禁用了赋值运算符1
ClosureType& operator=(const ClosureType&) = delete;
tips: 如果类不应该被拷贝或者移动,需要显式防止其被拷贝或者移动。否则,如果类有自定义的析构函数,需要显式的定义拷贝和移动函数。
1
2
3
4
5
6 > class X
> {
> X(const X&) = delete;
> X& operator=(const X&) = delete;
> };
>
但是没有禁用复制构造函数,所以仍然可以用是一个lambda表达式去初始化另一个(通过产生副本)。
关于lambda的捕捉块,主要有以下用法:
- []:默认不捕捉变量
- [=]:默认以值捕捉所有变量(最好不要用)
- [&]:默认以引用捕捉所有变量(最好不要用)
- :仅以值捕捉变量x,其他变量不捕捉
- [&x]:仅以引用捕捉x,其他变量不捕捉
- [=, &x]:默认以值捕捉所有变量,但是x是例外,通过引用捕捉
- [&, x]:默认以引用捕捉所有变量,但是x是例外,通过值捕捉
- [this]:通过引用捕捉当前对象(其实是复制指针)
- [* this]:通过传值方式捕捉当前对象
而lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。其实,最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过lambda表达式给出条件,传递给count_if函数:
1 | int value = 3; |
再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:1
2
3
4
5vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
此外,lambda表达式还用于对象的排序准则:1
2
3
4
5
6auto cmp = [](pair<string, size_t> &record1, pair<string, size_t> &record2)
{
return record1.second > record2.second;
};
std::sort(V.begin(), V.end(), cmp);
总之,对于大部分STL算法,可以非常灵活地搭配lambda表达式来实现想要的效果。
前面讲完了lambda表达式的基本使用,最后给出lambda表达式的完整语法:1
2
3
4
5
6
7// 完整语法
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }
// 可选的简化语法
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
第一个是完整的语法,后面3个是可选的语法。这意味着lambda表达式相当灵活,但是照样有一定的限制,比如你使用了拖尾返回类型,那么就不能省略参数列表,尽管其可能是空的。针对完整的语法,我们对各个部分做一个说明:
- capture-list:捕捉列表,这个不用多说,前面已经讲过,记住它不能省略;
- params:参数列表,可以省略(但是后面必须紧跟函数体);
- mutable:可选,将lambda表达式标记为mutable后,函数体就可以修改传值方式捕获的变量;
- constexpr:可选,C++17,可以指定lambda表达式是一个常量函数;
- exception:可选,指定lambda表达式可以抛出的异常;
- attribute:可选,指定lambda表达式的特性;
- ret:可选,返回值类型;
- body:函数执行体。