万岁!C++ 委员会听取了开发人员的意见,在 C++11 标准中加入了 lambda 表达式!
Lambda 表达式很快就成为现代 C++ 中最具辨识度的一个特性。
你可以在 N3337(C++11 的最终草案)中阅读其完整规范,以及关于 lambda 的单独部分:[express .prim.lambda]。
我认为委员会以一种聪明的方式在语言中添加了 lambda。他们设计了新的语法,但随后编译器将其“展开”为一个未命名的“隐藏的”函数对象类型。这样我们就拥有了真正强类型语言的所有优点(以及缺点),使代码理解起来更加容易。
在本章,你将会学习到:
- Lambda 的基础语法。
- 如何捕获一个变量。
- 如何捕获一个类的非静态成员变量。
- Lambda 的返回类型。
- 什么是闭包类型。
- 怎样将 lambda 表达式转换成一个函数指针从而能够去使用 C 风格的 API.
- 什么是 IIFE 以及为什么它是的有用的。
- 如何继承一个 lambda 表达式。
让我们出发吧!
Lambda 表达式的语法
下图说明了 C++11 中 lambda 的语法:现在让我们通过几个例子来感受一下它。
Lambda 表达式的几个例子
// 1. 最简单的 lambda 表达式:
[] {};
在第一个示例中,你可以看到一个“最迷你”的 lambda 表达式。它只需要[]
部分
(lambda 引入器),然后用空的{}
部分作为函数体。形参列表()
是可选的,在本例中不需要。
// 2. 拥有两个参数的 lambda:
[] (float f, int a) { return a * f; };
[] (int a, int b) { return a < b; };
在第二个例子中,可能是最常见的例子了,你可以看到参数都传递到()
部分,就像普通函数一样。返回类型不需要,因为编译器会自动推导它。
// 3. 尾置返回类型:
[] (MyClass t) -> int { auto a = t.compute(); print(a); return a; };
在上面的例子中,我们显式地设置了一个返回类型。后面的返回类型也可用在 C++11 以来的常规函数声明中。
// 4. 额外的说明符:
[x] (int a, int b) mutable { ++x; return a < b; };
[] (float param) noexcept { return param * param; };
[x] (int a, int b) mutable noexcept { ++x; return a < b; };
最后一个示例显示,在 lambda 的主体之前,可以使用其他说明符。在代码中,我们使用了 mutable
(这样我们可以改变捕获的变量)和noexcept
。第三个 lambda 使用了mutable
和noexcept
,并且它们必须以该顺序出现(你不能写noexcept
mutable
,因为编译器会拒绝它)。
虽然()
部分是可选的,但如果你想应用 mutable
或 noexcept
,此时()
则需要在出现的表达中:
// 5. 可选项
[x] { std::cout << x; }; // 不需要 ()
[x] mutable { ++x; }; // 无法通过编译!
[x] () mutable { ++x; }; // 可以,mutable 前面的 () 是必要的
[] noexcept { }; // 无法通过编译!
[] () noexcept { }; // 可以
同样的模式也适用于其他可以应用于 lambdas 的说明符,比如 C++17 中的 constexpr
和 C++20 中的 consteval
。
在熟悉了基本的例子之后,我们现在可以尝试去理解它是如何工作的,并学习 lambda 表达式的所有可能用法。
核心定义
在我们继续之前,从 C++ 标准中引入一些核心定义是很方便的:
来自 [expr.prim.lambda#2]
lambda 表达式的计算结果是一个临时的纯右值。这个临时值叫做闭包对象。
作为旁注,lambda 表达式是一个 prvalue 即“纯右值” 。这种类型的表达式通常产生自初始化并出现在赋值的右侧(或在 return 语句中)。阅读 C++ Reference,[express .prim.lambda#3] 中给出的的另一个定义是:
lambda 表达式的类型(也就是闭包对象的类型)是一个唯一的,未命名的非联合类类型——称为闭包类型。
编译器展开
从以上定义中,我们可以了解到编译器从一个 lambda 表达式生成唯一的闭包类型。然后我们可以通过这个类型来实例化出闭包对象。
以下示例展示了如何写一个 lambda 表达式并将其传给std::for_each
。为了便于比较,代码还说明了编译器生成的相应的函数对象类型:
// Ex2_1: Lambda 和 相应的函数对象。
#include
#include
#include
int main() {
struct {
void operator()(int x) const {
std::cout << x << '\n';
}
} someInstance;
const std::vector v { 1, 2, 3 };
std::for_each(v.cbegin(), v.cend(), someInstance);
std::for_each(v.cbegin(), v.cend(), [] (int x) {
std::cout << x << '\n';
}
);
}
在本例中,编译器将
[](int x) { std::cout << x << '\n'; }
翻译成一个匿名函数对象,简化形式如下:
struct {
void operator()(int x) const {
std::cout << x << '\n';
}
} someInstance;
“翻译”或“展开”的过程可以很容易地在 C++ Insights 在线网页工具上看到。该工具获取有效的 C++ 代码,然后产生编译器需要的源代码版本:像 lambda 的匿名函数对象,模板的实例化等其他 C++ 的特性。
在下一节中,我们将深入研究 lambda 表达式的各个部分。
Lambda 表达式的类型
由于编译器为每个 lambda (闭包类型)生成唯一的名称,所以我们就没法把它“拼写”在前面。
这就是为什么必须使用auto
(或 decltype
)来推断其类型。
auto myLambda = [](int a) -> double { return 2.0 * a; };
而且,如果你有两个看起来一样的 lambda:
auto firstLam = [](int x) { return x * 2; };
auto secondLam = [](int x) { return x * 2; };
它们的类型也是不同的,即使“代码背后”是相同的!编译器需要为这两个 lambda 声明的每个都生成惟一的匿名类型。我们可以用下面的代码来证明这个属性:
// Ex2_1: 相同的代码,不同的类型。
#include
int main() {
const auto oneLam = [](int x) noexcept { return x * 2; };
const auto twoLam = [](int x) noexcept { return x * 2; };
static_assert(!std::is_same::value,
"must be different!");
}
上面的例子验证了 oneLam 和 twoLam 的闭包类型是否不相同。
在 C++17 中我们可以使用无需消息的
static_assert
以及用于类型萃取的辅助变量模板is_same_v
:static_assert(std::is_same_v
);
然而,虽然你不知道确切的名称,但是你还是可以拼出 lambda 的签名,然后将其存储在std::function
中。一般来说,如果 lambda 是通过 std::function<>
类型“表示”的,那么它可以完成定义为auto
的 lambda 无法完成的任务。例如,前面的 lambda 具有double(int)
的签名,因为它接受int
作为输入参数并返回double
。然后我们可以用以下方法创建std::function
对象:
std::function
std::function
是一个重量级的对象,因为它需要处理所有可调用对象。要做到这一点,它需要高级的内部机制,如类型双关语,甚至是动态内存分配。我们可以通过一个简单的实验来检验它的大小:
// Ex2_3: std::function 和 auto 类型推导。
#include
#include
int main() {
const auto myLambda = [](int a) noexcept -> double {
return 2.0 * a;
};
const std::function myFunc =
[](int a) noexcept -> double {
return 2.0 * a;
};
std::cout << "sizeof(myLambda) is " << sizeof(myLambda) << '\n';
std::cout << "sizeof(myFunc) is " << sizeof(myFunc) << '\n';
return myLambda(10) == myFunc(10);
}
在 GCC 编译下代码输出如下:
sizeof(myLambda) is 1
sizeof(myFunc) is 32
因为 myLambda 只是一个无状态的 lambda,所以它也是一个空类,没有任何数据成员字段,所以它的最小大小只有一个字节。另一边的std::function
版本则要大得多——32 个字节。这就是为什么如果可以的话,应该依靠自动类型推导来获得尽可能小的闭包对象。
当我们讨论std::function
时,还需要注意的是,这种类型不是只移型闭包。你可以在 C++14 的可移动的类型章节中阅读关于这个问题的更多信息。