C++ lambda表达式与函数对象
lambda
表达式是C++11
中引入的一项新技术,利用lambda
表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。但是从本质上来讲,lambda
表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现。但是它简便的语法却给C++
带来了深远的影响。如果从广义上说,lamdba
表达式产生的是函数对象。在类中,可以重载函数调用运算符()
,此时类的对象可以将具有类似函数的行为,我们称这些对象为函数对象(Function Object)或者仿函数(Functor)。相比lambda
表达式,函数对象有自己独特的优势。下面我们开始具体讲解这两项黑科技。
lambda表达式
我们先从简答的例子开始,我们定义一个可以输出字符串的lambda
表达式,表达式一般都是从方括号[]
开始,然后结束于花括号{}
,花括号里面就像定义函数那样,包含了lamdba
表达式体:
// 定义简单的lambda表达式
auto basicLambda = [] { cout << "Hello, world!" << endl; };
// 调用
basicLambda(); // 输出:Hello, world!
上面是最简单的lambda
表达式,没有参数。如果需要参数,那么就要像函数那样,放在圆括号里面,如果有返回值,返回类型要放在->
后面,即拖尾返回类型,当然你也可以忽略返回类型,lambda
会帮你自动推断出返回类型:
// 指明返回类型
auto add = [](int a, int b) -> int { return a + b; };
// 自动推断返回类型
auto multiply = [](int a, int b) { return a * b; };
int sum = add(2, 5); // 输出:7
int product = multiply(2, 5); // 输出:10
大家可能会想lambda
表达式最前面的方括号的意义何在?其实这是lambda
表达式一个很要的功能,就是闭包。这里我们先讲一下lambda
表达式的大致原理:每当你定义一个lambda
表达式后,编译器会自动生成一个匿名类(这个类当然重载了()
运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda
表达式就会返回一个匿名的闭包实例,就是一个右值。所以,我们上面的lambda
表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda
捕捉块。看下面的例子:
int main()
{
int x = 10;
auto add_x = [x](int a) { return a + x; }; // 复制捕捉x
auto multiply_x = [&x](int a) { return a * x; }; // 引用捕捉x
cout << add_x(10) << " " << multiply_x(10) << endl;
// 输出:20 100
return 0;
}
lambda
捕捉块为空时,表示没有捕捉任何变量。但是上面的
add_x
是以
复制的形式捕捉变量x
,而
multiply
是以
引用的方式捕捉x
。前面讲过,
lambda
表达式是产生一个闭包类,那么捕捉是回事?对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。前面说过,闭包类也实现了函数调用运算符的重载,一般情况是:
class ClosureType
{
public:
// ...
ReturnType operator(params) const { body };
}
这意味着lambda
表达式无法修改通过复制形式捕捉的变量,因为函数调用运算符的重载方法是const
属性的。有时候,你想改动传值方式捕获的值,那么就要使用mutable
,例子如下:
int main()
{
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; }; // 复制捕捉x
cout << add_x(10) << endl; // 输出 30
return 0;
}
这是为什么呢?因为你一旦将lambda
表达式标记为mutable
,那么实现的了函数调用运算符是非const属性的:
class ClosureType
{
public:
// ...
ReturnType operator(params) { body };
}
对于引用捕获方式,无论是否标记mutable
,都可以在lambda
表达式中修改捕获的值。至于闭包类中是否有对应成员,C++
标准中给出的答案是:不清楚的,看来与具体实现有关。既然说到了深处,还有一点要注意:lambda
表达式是不能被赋值的:
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本
你可能会想a
与b
对应的函数类型是一致的(编译器也显示是相同类型:lambda [] void () -> void),为什么不能相互赋值呢?因为禁用了赋值操作符:
ClosureType& operator=(const ClosureType&) = delete;
但是没有禁用复制构造函数,所以你仍然可以用一个lambda
表达式去初始化另外一个lambda
表达式而产生副本。并且lambda
表达式也可以赋值给相对应的函数指针,这也使得你完全可以把lambda
表达式看成对应函数类型的指针。
闲话少说,归入正题,捕获的方式可以是引用也可以是复制,但是具体说来会有以下几种情况来捕获其所在作用域中的变量:
- []:默认不捕获任何变量;
- [=]:默认以值捕获所有变量;
- [&]:默认以引用捕获所有变量;
- [x]:仅以值捕获x,其它变量不捕获;
- [&x]:仅以引用捕获x,其它变量不捕获;
- [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
- [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
- [this]:通过引用捕获当前对象(其实是复制指针);
- [*this]:通过传值方式捕获当前对象;
在上面的捕获方式中,注意最好不要使用[=]
和[&]
默认捕获所有变量。首先说默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的声明周期:
后续补充完整,链接:
https://www.jianshu.com/p/d686ad9de817