介绍 lambda
我们在自定义排序中经常看到如下代码:
sort(nums.begin(), nums.end(), [](const int& a, const int& b) {
return a > b;
});
这个代码中使用的是 sort 的带二个版本,这个版本是重载过的,它接受三个参数,第三个参数是一个谓词。
谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。接受谓词参数的算法对输入序列中的元素调用元素。对应上述的自定义排序算法,该算法会将数组 nums
中的元素进行降序排序。
你可以这样理解:
- 对于数组中需要比较的两个元素
a
和b
,如果a > b
为真,那么a
就会被放在b
前面,也就是大的数放在数组中前面位置,小的数放在数组靠后位置,数组表现出降序状态。 - 如果
a > b
为假,那么a
就会被放在b
后面,排序呈升序状态。
标准库算法所使用的谓词分为两类:一元谓词和二元谓词。一元谓词和二元谓词对应的只能接受单一参数和两个参数。但是有时我们希望进行的操作需要更多的参数,超过了算法对谓词的限制。于是,引入了 lambda 表达式。
lambda 表达式的形式
lambda 表达式是 C++11 标准加入的一个重要的新特性,它提供了一种在代码中很方便定义函数的方法。写法很简洁,一个 lambda 表达式具有如下形式:
[capture list] (parameter list) -> return type {function body};
- parameter list:与普通函数一样表示参数列表,参数列表可忽略(等价于指定一个空参数列表);不同于普通函数的是,lambda 不能有默认参数。
- capture list:捕获列表,是一个 lambda 所在函数中定义的局部变量的列表。
- return type:返回类型,如果函数体包含任何单一 return 之外的内容,且未指定返回值,则返回 void。
接下来对 向 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();
}
与值传递参数类似,采用值捕获的前提是变量是可拷贝的。
与参数不同,被捕获的变量的值在 lambda 创建时拷贝,而不是调用时拷贝。在此例子中,由于被捕获变量的值 v1
是在 lambda 创建时拷贝,因此随后对其修改不会影响 lambda 内对应的值。最终的 j = 42
,保存了我们创建它时 v1
的拷贝。
可调用对象
另外要说一下,lambda 表达式是一个可调用对象,可以直接对其使用调用运算符(即 ())。另外还有三种可调用对象:函数、函数指针和重载了调用运算符的类。
引用捕获
我们定义 lambda 时可以采用引用方式捕获变量。
void fcn2 () {
size_t v1 = 42;
// 对象 f2 包含 v1 的引用
auto f2 = [&v1] {
return v1;
};
v1 = 0;
auto j = f2(); // j 为 0: f2 保存对 v1 的引用,而非拷贝
}
v1
之前的 &
指出 v1
应该以引用方式捕获。当我们在 lambda 函数体内使用引用捕获的变量时,实际上使用的是引用绑定的对象。在此例子中,当 lambda 返回 v1
时,它返回的是 v1
指向的对象的值。因为通过 v1 = 0
,改变了对象的值,所以最终有 就= 0
。
使用限制
引用捕获与返回引用有着相同的问题和限制。在返回引用中,一定不能返回局部变量的引用。
在 lambda 表达式中,如果我们采用引用方式捕获一个变量,必须保证在 lambda 执行时变量是存在的。lambda 捕获的都是局部变量,这些局部变量在函数结束后就不存在了。如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
必要性
有些时候使用引用捕获是必要的。比如在捕获 ostream 对象时,由于我们不能拷贝 ostream 对象,因而捕获 os(ostream 的实例化) 的唯一方法就是引用捕获。
隐式捕获
除了显示列出我们希望使用的来自函数的变量之外,还可以让编译器根据 lambda 函数体中的代码自动推导出我们要使用哪些变量。此时使用 &
或 =
指示编译器推断捕获列表。&
告诉编译器采用引用捕获方式,=
则表示采用值捕获方式。
混合捕获
如果我们希望一部分采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显示捕获。混合使用隐式捕获和显示捕获时,有以下几点注意事项:
- 捕获列表中的第一个元素必须是一个
&
或=
。 - 显示捕获的变量必须使用与隐式捕获不同的方式。比如,隐式捕获采用的是引用方式,则显示捕获必须采用值捕获方式。
指定 lambda 返回类型
返回 void 类型
默认情况下,如果一个 lambda 函数体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void。
先看一个简单的例子,我们使用标准库中的 transform \texttt{transform} transform 算法和 lambda 将一个序列中的每个负数都替换成其绝对值。
tansform(vi.begin(), vi.end(), vi.begin(), [](int i) {
return i < 0 ? -i : i;
});
transform \texttt{transform} transform 接受三个迭代器和一个可调用对象(lambda)。前两个迭代器表示输入序列,第三个迭代器表示结果写入的目的位置。 transform \texttt{transform} transform 将输入序列中每个元素替换为可调用对象操作该该元素得到的结果。
在本例中,我们传递给 transform \texttt{transform} transform 一个 lambda,它返回其参数的绝对值。lambda 函数体中是一个条件表达式,一个单一的 return 语句。我们无须指定返回类型,因为可以自动推导出 void 类型。
但是,如果我们将程序改写成看起来等价的 if 语句,就会产生产生编译错误。以下代码实测,编译器并不会报错。
tansform(vi.begin(), vi.end(), vi.begin(), [](int i) {
if (i < 0) {
return -i;
}
return i;
});
编译器推断出这个版本的 lambda 返回类型为 void,但是它返回了一个 int 值。
注:根据《C++ Primer》lambda 表达式一节的说法,如果一个 lambda 函数体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void。但是在代码实测中,编译器推断出这个版本的 lambda 返回类型为 void,而实际上 lambda 表达式返回一个未通过 尾置 法指定返回类型的非 void 类型,编译器一不会报错。
使用尾置返回类型
当我们需要为一个 lambda 定义返回类型时,必须使用 尾置返回类型。
tansform(vi.begin(), vi.end(), vi.begin(), [](int i) -> int {
if (i < 0) {
return -1;
}
return i;
});
尾置返回类型是
C++11
\texttt{C++11}
C++11 标准中引入的一种新特性。任何函数的定义都能使尾置返回,但是这种形式对于返回值类型比较复杂的函数最有效,比如返回类型是数组的指针或者数数组的引用。尾置返回类型跟在形参列表之后以一个 ->
开头,->
后面就是返回类型。