在使用的地方直接写函数
上一页你看到了重载如何让多个函数共用一个名字。但有时你根本不需要一个具名函数:你只需要在某个地方用一次一小段逻辑,给它起名字只会让代码更杂乱。这就是 lambda:一种可以内联定义的匿名函数。
lambda 有一个独特的四段式结构:
[capture](parameters) -> return_type { body }
看到 [],就知道这是一个 lambda。返回类型是可选的,编译器通常会自动推导。下面是最简单的一个:
greet 只是一个变量(它的类型无法直接写出,所以用 auto 保存),可以用 () 调用它。带参数的 lambda 和普通函数的用法完全一样:
捕获:访问外围作用域
让 lambda 不只是“没有名字的函数”的,是捕获列表,也就是 []。它让 lambda 除了自己的参数之外,还能使用定义它的作用域中的变量。
用 [x] 按值捕获:lambda 得到它自己的一个副本,固定为 lambda 创建那一刻的值。
注意 scale(5) 打印的是 50,用的是创建 lambda 时 factor 的值 10。按值捕获相当于拍了一张快照。
用 [&x] 按引用捕获:lambda 引用的是原始变量,能看到之后的修改,也可以修改它。
你还可以用 [=](全部按值)或 [&](全部按引用)捕获 lambda 用到的所有变量。这样写很方便,但显式地写出 [total] 或 [&total],能清楚地表明 lambda 到底用到了什么,也更容易推断代码的行为。
悬空引用陷阱
按引用捕获既强大又危险。引用只在原始变量存活期间有效。如果 lambda 活得比它捕获的变量更久,就会得到悬空引用,导致未定义行为:程序可能崩溃,可能打印出垃圾值,也可能碰巧看起来正常。
下面是一个经典错误:返回一个按引用捕获局部变量的 lambda。
auto makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // BUG: count dies here
}
// The returned lambda now references destroyed memory.
makeCounter 返回时,它的局部变量 count 被销毁,但 lambda 仍然持有对它的引用。调用返回的 lambda 就会访问已经失效的内存。解决办法是按值捕获,让 lambda 拥有自己的状态:
经验法则:只有当 lambda 会被立即在本地使用时(比如下文的算法),才按引用捕获。一旦 lambda 要被保存、返回或稍后执行,就优先按值捕获。
mutable 与返回类型
你注意到上一个例子中的 mutable 了吗?默认情况下,按值捕获的变量在 lambda 内部是 const 的:可以读取副本,但不能修改。加上 mutable,lambda 就能在多次调用之间修改自己的副本。
mutable 只影响 lambda 私有的副本,外部的 seen 保持不变,而这正是按值捕获的意义所在。
大多数时候,编译器都能很好地推导出返回类型。只有在存在歧义时才需要用 -> 明确写出来,比如一个在不同分支可能返回不同类型的 lambda:
// Without -> the compiler can't decide between int and double
auto half = [](int n) -> double {
if (n % 2 == 0) return n / 2; // int
return n / 2.0; // double
};
lambda 与算法:真正的用武之地
C++ 引入 lambda,就是为了把简短的逻辑交给标准库算法。在有 lambda 之前,你必须在远离使用位置的地方单独写一个具名函数或一个笨重的函数对象。现在,逻辑就写在调用的地方。
最常见的例子是自定义排序顺序:
捕获在这里大放异彩,因为 lambda 可以引入一个值,用来过滤或计数。下面的代码统计有多少个分数超过了用户设定的及格线:
由于这些 lambda 是立即使用的,不会比外围函数活得更久,所以在这里按引用捕获([&passMark])也是安全的;不过按值捕获同样清晰,而且永远不会悬空。
下一步:指针
lambda 悄悄引出了一个更深层的问题:当你捕获 [&x] 时,lambda 保存的是 x 的位置,而这个位置只在 x 存活期间有效。一个值指向某个东西在内存中的位置,以及当它指向的东西消失时会发生什么,这正是下一页的内容。我们将正面认识指针:如何获取地址,如何通过地址访问数据,以及你刚刚看到的悬空问题如何出现在 C++ 的方方面面。
常见问题
C++ 中的 lambda 是什么?
lambda 是一种可以内联书写的匿名函数,就写在你使用它的地方。语法是 [captures](parameters){ body }。它非常适合简短的一次性操作,比如传给 std::sort 的比较逻辑,不必在别处另外声明一个具名函数。
C++ lambda 中按值捕获和按引用捕获有什么区别?
[x] 捕获的是 x 的一个副本,固定为 lambda 创建那一刻的值。[&x] 捕获的是原始 x 的引用,所以 lambda 能看到之后的修改,也可以修改它。只有在能保证被捕获的变量比 lambda 活得更久时才使用 [&],否则会得到悬空引用。
为什么我的 C++ lambda 提示不能修改捕获的变量?
按值捕获的变量在 lambda 内部默认是 const 的。加上 mutable 关键字,比如 [x]() mutable { x++; },lambda 就能修改它自己的副本。注意这只会改变 lambda 的副本,而不会改变外部的原始变量。