C++ 中的 lambda 表达式
lambda 表达式——《C++ Primer》笔记
介绍 lambda
首先回顾一下可调用对象的概念:我们说,如果对于一个对象或者一个表达式,可以对其使用调用运算符 () ,则称它为可调用对象。
可调用对象有这些:
- 函数
- 函数指针
- 重载了调用运算符的类(也称为函数对象)
- lambda 表达式
那么什么是 lambda 表达式呢?lambda 表达式可以理解成一个未命名的内联函数。它具有返回类型、参数列表、函数体。形如这样:[capture list] (parameter list) -> return type { function body }
capture list 也就是捕获列表,是一个 lambda 所在函数中定义的局部变量的列表(通常为空)。与普通函数所不同,lambda 表达式必须使用尾置返回类型。
tip: 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表(即使它为空)和函数体。
如果 lambda 的函数体只包含一个 return 语句,则返回类型可以从返回的表达式类型中推断出来。否则,返回类型为 **void** 。
// 单一的 return 语句,推断返回类型为 int
auto f = [] { return 42; };
std::cout << f() << std::endl;
lambda 的原理浅讲
lambda 底层到底是怎么实现的呢?
当我们定义一个 lambda 时,编译器就会生成一个与 lambda 对应的新的(未命名的)类类型。(后面再会补充细讲)
默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员。lambda 的数据成员在 lambda 对象创建时被初始化。
lambda 捕获
lambda 捕获方式有两种:值捕获跟引用捕获。
值捕获
跟普通函数传递参数类似,lambda 使用值捕获的前提是变量可以拷贝。由我们上文讲到的 lambda 实现方式,我们可以知道,被捕获的变量的值在 lambda 创建时拷贝,而不是调用时拷贝。
举个代码例子:
void fcn1() {
size_t v1 = 42;
auto f = [v1] { return v1; };
v1 = 0;
auto j = f(); // j 为42
}
由于被捕获变量的值在 lambda 创建时拷贝,因此随后对其修改不会影响到 lambda 内对应的值。
引用捕获
我们定义 lambda 表达式时可以采用引用方式来捕获变量。
void fcn2() {
size_t v1 = 42;
auto f2 = [&v1] { return v1; };
v1 = 0;
auto j = f2(); // j 为 0
}
一个特别要注意的点是:当以引用方式捕获一个变量时,必须保证在 lambda 执行时变量是存在的。
由于这个原因,当我们使用一个函数返回一个 lambda 时,此 lambda 不能包含引用捕获。
捕获的一些注意点
一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。
原因在于:如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在 lambda 执行时,绑定到迭代器、指针或者引用的对象仍然存在,而且需要保证对象具有预期的值。
void fcn3() {
int *a = new int(10);
auto f = [a] { return *a; };
*a = 10;
auto j = f(); // 预期中 a 应该是10,现在却是0;
}
void fcn4() {
int *a = new int(10);
auto f = [a] { return *a; };
delete a;
auto j = f(); // 错误:a指向的对象已经不存在了
}
隐式捕获
我们可以让编译器根据 lambda 函数体中的代码来推断我们要使用哪些变量。这时候我们要在捕获列表中写一个&或者=。
&告诉编译器采用引用捕获方式=告诉编译器采用值捕获方式
void fcn5() {
int v1 = 42;
// 隐式捕获 v1,捕获方式为值捕获
auto f = [=] { return v1; };
auto j = f();
}
更加有意思的是,我们还可以混用隐式捕获和显式捕获。但是我们必须接受一些限制:
- 捕获列表中的第一个元素必须是一个
&或者= - 显式捕获的变量必须使用与隐式捕获不同的捕获方式。也就是如果隐式捕获是引用捕获,那么显式捕获就必须是值捕获;如果隐式捕获是值捕获,那么显式捕获必须是引用捕获。
void fcn6() {
std::ostream &os = std::cout;
char c = ' ';
// os 为隐式捕获, c 为显式捕获
auto f = [&, c] (const string &str) { os << str << c; };
// c 为隐式捕获,os 为隐式捕获
auto g = [=, &os] (const string &str) { os << str << c; };
f("Hello,");
g("World!");
}
可变 lambda
默认情况下,对于一个被值拷贝的变量,lambda 不会改变它的值。如果我们希望能改变一个被捕获变量的值,就必须在参数列表首加上关键字 **mutable**。因此,可变 lambda 不能忽略参数列表(但可以为空)。
void fcn7() {
size_t v1 = 42;
auto f = [v1] () mutable { return ++v1; };
v1 = 0;
auto j = f(); // j 为43
}
一个引用捕获的变量能否修改依赖于此引用指向的是一个 const 类型还是一个非 const 类型:
void fcn8() {
size_t v1 = 42;
auto f2 = [&v1] { return ++v1; };
v1 = 0;
auto j = f2(); // j 为1
}
lambda 返回类型
默认情况下,如果一个 lambda 函数体内包含 return 之外的任何语句,则编译器假定此 lambda 返回void,被推断返回void的 lambda 不能返回值。
当我们需要为一个 lambda 定义返回类型时,必须使用尾置返回类型。
void fcn9() {
auto f = [] (int i) -> int
{ if (i < 0) return -i;
else return i;
};
auto j = f(-1); // j 为1
}