什么是 Lambda 表达式?

面试官: 好的,我们来聊聊 C++11 引入的一个非常强大的特性——Lambda 表达式

你能用自己的话简单解释一下,Lambda 表达式是什么吗?它主要解决了什么问题,或者说,它为我们编写 C++ 代码带来了什么样的便利?

回答:

  • 我的理解是,lambda 表达式就是“匿名函数”,即省略名字的函数。
  • 它定义了一个函数,这个函数有函数的所有功能,但不需要名字。
  • 这让我们在需要写一些小巧的工具性一次性函数时,省去了从头定义函数的麻烦,让代码看上去更加简洁。

面试官 (补充与肯定): 你说的完全正确。Lambda 表达式的出现,极大地改变了 C++ 的编程风格,特别是与标准库算法(STL Algorithms)结合使用时。

在 C++11 之前,如果我们需要给 std::sort, std::find_if 这样的算法传递一个自定义的比较或判断逻辑,我们通常需要:

  1. 在别处定义一个完整的具名函数。
  2. 或者定义一个函数对象(一个重载了 operator()structclass)。

这两种方式都比较繁琐,并且会导致逻辑和使用点的分离

而 Lambda 表达式,正如你所说,允许我们在需要的地方,就地定义一个匿名的、小巧的函数,让代码更紧凑、可读性也更强。


Lambda 表达式的捕获列表

面试官 (深入提问): 好的,你对 Lambda 的基本概念很清楚。我们来看一下 Lambda 表达式的语法结构。

一个完整的 Lambda 表达式看起来像这样: [ captures ] ( params ) -> return_type { body }

其中,[ captures ] 这个部分,我们称之为捕获列表 (Capture List),这是 Lambda 表达式最强大、也最核心的部分。

请问,这个捕获列表是用来做什么的?值捕获 (Capture by Value)引用捕获 (Capture by Reference) 有什么区别?你能举例说明一下吗?

例如:

1
2
3
4
5
6
7
int x = 10;
int y = 20;

// 值捕获 x,引用捕获 y
auto my_lambda = [x, &y] {
    // ...
};

回答:

  • 我的理解是,捕获列表用于获取外部已有的变量资源。
  • 值捕获,就是以值访问的形式使用该资源,相当于复制一份资源,在 lambda 表达式内自己用。
  • 引用捕获,就是用引用访问的形式使用该资源,相当于使用同一份资源。

面试官 (总结与举例): 你说得完全正确。

捕获列表 [...] 的核心作用,就是让这个匿名的、局部的 Lambda 函数,能够访问并使用其定义时所在作用域中的外部变量。

我们来通过具体的例子,深化一下你对两者区别的理解:

值捕获 (Capture by Value) [x]:

  • 行为:在 Lambda 创建的那一刻,会拷贝一份 x副本,并存储在 Lambda 内部。
  • 特点
    • Lambda 内部操作的是这个副本,不会影响外部原始的 x
    • 默认情况下,这个副本在 Lambda 内部是 const 的,你不能修改它。如果你想修改这个内部副本,需要加上 mutable 关键字:[x]() mutable { x++; }
  • 示例
    1
    2
    3
    4
    5
    6
    
    int x = 10;
    auto lambda_val = [x]() { 
        std::cout << "Inside (value capture): " << x << std::endl; 
    };
    x = 100; // 修改外部的 x
    lambda_val(); // 调用 Lambda
    
    输出Inside (value capture): 10。因为它使用的是创建时拷贝的那个旧值 10

引用捕获 (Capture by Reference) [&y]:

  • 行为:Lambda 内部存储的是对外部变量 y 的一个引用
  • 特点
    • Lambda 内部对 y 的任何修改,都会直接影响外部原始的 y
    • 生命周期风险:你必须保证,在 Lambda 被调用的时候,它所引用的那个外部变量 (y) 依然是存活的、有效的。如果 y 已经被销毁了,再去调用这个 Lambda,就会导致悬挂引用 (Dangling Reference),程序会崩溃。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    
    int y = 20;
    auto lambda_ref = [&y]() { 
        y++; //可以直接修改
        std::cout << "Inside (reference capture): " << y << std::endl; 
    };
    y = 200; // 修改外部的 y
    lambda_ref(); // 调用 Lambda
    
    输出Inside (reference capture): 201。因为它操作的是外部的 y,先被改为 200,然后在 Lambda 内部 ++ 变成了 201

除了单个捕获,还有一些简写形式:

  • [=]: 以方式捕获所有在 Lambda 内部被使用的外部变量。
  • [&]: 以引用方式捕获所有在 Lambda 内部被使用的外部变量。
  • [=, &y]: 默认值捕获,但 y 单独指定为引用捕获。
  • [&, x]: 默认引用捕获,但 x 单独指定为值捕获。

你对 Lambda 捕获机制的理解已经非常到位了。


泛型 Lambda

面试官 (最终问题): 好的,既然我们理解了 Lambda。在 C++14 之后,Lambda 有了一个很酷的升级,叫做泛型 Lambda (Generic Lambda)

它的语法是,在参数列表里使用 auto 关键字。

1
2
3
auto generic_lambda = [](auto a, auto b) {
    return a + b;
};

请问,这种泛型 Lambda,它的本质是什么?它和我们熟悉的函数模板 (Function Template) 有什么相似之处?

回答:

  • 我觉得泛型 Lambda 本质就是一种特殊的函数模板。它也没有规定参数的类型,而是让编译器去自行匹配。

面试官 (总结与肯定): 你说得对极了。

一个泛型 Lambda:

1
2
3
auto generic_lambda = [](auto a, auto b) {
    return a + b;
};

在编译器看来,它几乎等价于在一个匿名函数对象(Closure Type)内部,定义了一个成员函数模板

大致可以理解成这样(这只是为了帮助理解,不是真实的实现):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class __SomeUniqueLambdaName {
public:
    // 编译器为你的泛型 Lambda 生成了一个成员函数模板
    template<typename T1, typename T2>
    auto operator()(T1 a, T2 b) const {
        return a + b;
    }
};

auto generic_lambda = __SomeUniqueLambdaName{};

相似之处:

  • 类型推导:和函数模板一样,泛型 Lambda 也是在调用时,编译器才根据传入的实参,自动推导auto 应该代表的具体类型。
    1
    2
    3
    
    generic_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::tuplestd::variant)进行同样的操作,会非常麻烦。而泛型 Lambda 让这一切变得轻而易举。

一句话总结:泛型 Lambda 就是一个“语法糖 (Syntactic Sugar)”,让我们能用更少的代码,写出更强大的、类似函数模板的匿名函数。


总结

面试结束

今天我们深入探讨了现代 C++ 的一个核心特性——Lambda 表达式。

  • 我们理解了它的基本概念(匿名函数)。
  • 掌握了它最强大的捕获机制(值捕获 vs 引用捕获)。
  • 并最终揭示了泛型 Lambda函数模板之间的深刻联系。