什么是 Lambda 表达式?
面试官: 好的,我们来聊聊 C++11 引入的一个非常强大的特性——Lambda 表达式。
你能用自己的话简单解释一下,Lambda 表达式是什么吗?它主要解决了什么问题,或者说,它为我们编写 C++ 代码带来了什么样的便利?
回答:
- 我的理解是,lambda 表达式就是“匿名函数”,即省略名字的函数。
- 它定义了一个函数,这个函数有函数的所有功能,但不需要名字。
- 这让我们在需要写一些小巧的工具性一次性函数时,省去了从头定义函数的麻烦,让代码看上去更加简洁。
面试官 (补充与肯定): 你说的完全正确。Lambda 表达式的出现,极大地改变了 C++ 的编程风格,特别是与标准库算法(STL Algorithms)结合使用时。
在 C++11 之前,如果我们需要给 std::sort, std::find_if 这样的算法传递一个自定义的比较或判断逻辑,我们通常需要:
- 在别处定义一个完整的具名函数。
- 或者定义一个函数对象(一个重载了
operator()的struct或class)。
这两种方式都比较繁琐,并且会导致逻辑和使用点的分离。
而 Lambda 表达式,正如你所说,允许我们在需要的地方,就地定义一个匿名的、小巧的函数,让代码更紧凑、可读性也更强。
Lambda 表达式的捕获列表
面试官 (深入提问): 好的,你对 Lambda 的基本概念很清楚。我们来看一下 Lambda 表达式的语法结构。
一个完整的 Lambda 表达式看起来像这样:
[ captures ] ( params ) -> return_type { body }
其中,[ captures ] 这个部分,我们称之为捕获列表 (Capture List),这是 Lambda 表达式最强大、也最核心的部分。
请问,这个捕获列表是用来做什么的?值捕获 (Capture by Value) 和引用捕获 (Capture by Reference) 有什么区别?你能举例说明一下吗?
例如:
| |
回答:
- 我的理解是,捕获列表用于获取外部已有的变量资源。
- 值捕获,就是以值访问的形式使用该资源,相当于复制一份资源,在 lambda 表达式内自己用。
- 引用捕获,就是用引用访问的形式使用该资源,相当于使用同一份资源。
面试官 (总结与举例): 你说得完全正确。
捕获列表 [...] 的核心作用,就是让这个匿名的、局部的 Lambda 函数,能够访问并使用其定义时所在作用域中的外部变量。
我们来通过具体的例子,深化一下你对两者区别的理解:
值捕获 (Capture by Value) [x]:
- 行为:在 Lambda 创建的那一刻,会拷贝一份
x的副本,并存储在 Lambda 内部。 - 特点:
- Lambda 内部操作的是这个副本,不会影响外部原始的
x。 - 默认情况下,这个副本在 Lambda 内部是
const的,你不能修改它。如果你想修改这个内部副本,需要加上mutable关键字:[x]() mutable { x++; }。
- Lambda 内部操作的是这个副本,不会影响外部原始的
- 示例:输出:
1 2 3 4 5 6int x = 10; auto lambda_val = [x]() { std::cout << "Inside (value capture): " << x << std::endl; }; x = 100; // 修改外部的 x lambda_val(); // 调用 LambdaInside (value capture): 10。因为它使用的是创建时拷贝的那个旧值10。
引用捕获 (Capture by Reference) [&y]:
- 行为:Lambda 内部存储的是对外部变量
y的一个引用。 - 特点:
- Lambda 内部对
y的任何修改,都会直接影响外部原始的y。 - 生命周期风险:你必须保证,在 Lambda 被调用的时候,它所引用的那个外部变量 (
y) 依然是存活的、有效的。如果y已经被销毁了,再去调用这个 Lambda,就会导致悬挂引用 (Dangling Reference),程序会崩溃。
- Lambda 内部对
- 示例:输出:
1 2 3 4 5 6 7int y = 20; auto lambda_ref = [&y]() { y++; //可以直接修改 std::cout << "Inside (reference capture): " << y << std::endl; }; y = 200; // 修改外部的 y lambda_ref(); // 调用 LambdaInside (reference capture): 201。因为它操作的是外部的y,先被改为200,然后在 Lambda 内部++变成了201。
除了单个捕获,还有一些简写形式:
[=]: 以值方式捕获所有在 Lambda 内部被使用的外部变量。[&]: 以引用方式捕获所有在 Lambda 内部被使用的外部变量。[=, &y]: 默认值捕获,但y单独指定为引用捕获。[&, x]: 默认引用捕获,但x单独指定为值捕获。
你对 Lambda 捕获机制的理解已经非常到位了。
泛型 Lambda
面试官 (最终问题): 好的,既然我们理解了 Lambda。在 C++14 之后,Lambda 有了一个很酷的升级,叫做泛型 Lambda (Generic Lambda)。
它的语法是,在参数列表里使用 auto 关键字。
| |
请问,这种泛型 Lambda,它的本质是什么?它和我们熟悉的函数模板 (Function Template) 有什么相似之处?
回答:
- 我觉得泛型 Lambda 本质就是一种特殊的函数模板。它也没有规定参数的类型,而是让编译器去自行匹配。
面试官 (总结与肯定): 你说得对极了。
一个泛型 Lambda:
| |
在编译器看来,它几乎等价于在一个匿名函数对象(Closure Type)内部,定义了一个成员函数模板。
大致可以理解成这样(这只是为了帮助理解,不是真实的实现):
| |
相似之处:
- 类型推导:和函数模板一样,泛型 Lambda 也是在调用时,编译器才根据传入的实参,自动推导出
auto应该代表的具体类型。1 2 3generic_lambda(1, 2); // auto 被推导为 int, int generic_lambda(1.5, 2.5); // auto 被推导为 double, double generic_lambda(std::string("a"), std::string("b")); // auto 被推导为 string, string - 代码生成:每次用不同的类型调用泛型 Lambda 时,编译器都会实例化出一个新的、对应具体类型的函数版本。
- 泛型能力:它们都使得我们能够编写一段与具体类型无关的、通用的逻辑代码。
为什么要有泛型 Lambda?
它提供了一种极其简洁的方式,来定义一个“小巧”的、局部的函数模板。在 C++11 中,如果你想在 std::for_each 里对一个包含 int, double, string 多种类型的异构容器(比如 std::tuple 或 std::variant)进行同样的操作,会非常麻烦。而泛型 Lambda 让这一切变得轻而易举。
一句话总结:泛型 Lambda 就是一个“语法糖 (Syntactic Sugar)”,让我们能用更少的代码,写出更强大的、类似函数模板的匿名函数。
总结
面试结束
今天我们深入探讨了现代 C++ 的一个核心特性——Lambda 表达式。
- 我们理解了它的基本概念(匿名函数)。
- 掌握了它最强大的捕获机制(值捕获 vs 引用捕获)。
- 并最终揭示了泛型 Lambda 与函数模板之间的深刻联系。