7. Lambda 基础语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }

Lambda 表达式,说白了就是 C++11 引入的一种「匿名函数对象」。你不需要给它起名字,也不需要单独写一个函数或者仿函数类。直接在需要的地方写一段逻辑,编译器会帮你生成一个对应的函数对象。

我第一次接触 Lambda 时,觉得这玩意儿不就是语法糖吗?后来在项目里重构回调代码,才发现它有多香。嗯,咱们今天就把它的骨架拆开看看。

7.1 语法结构总览

Lambda 的完整语法长这样:

[捕获列表](参数列表) -> 返回类型 { 函数体 }

四个部分,缺一不可?其实不是。有些部分可以省略。我习惯把它拆成三块来看:

  • 捕获列表:决定 Lambda 能访问外部哪些变量
  • 参数列表 + 返回类型:决定调用时的接口
  • 函数体:具体执行的逻辑

你想想看,这跟普通函数最大的区别在哪?普通函数没有捕获列表。Lambda 可以「记住」它定义时的上下文变量,这是它最核心的能力。

7.2 捕获列表详解

捕获列表是 Lambda 的灵魂。我见过不少新手在这里踩坑,包括我自己早期也犯过迷糊。

捕获方式 写法 说明
值捕获 [x] 捕获变量 x 的副本,Lambda 内部修改不影响外部
引用捕获 [&x] 捕获变量 x 的引用,内部修改会反映到外部
隐式值捕获 [=] 按值捕获所有用到的外部变量
隐式引用捕获 [&] 按引用捕获所有用到的外部变量
混合捕获 [=, &x] 除 x 按引用外,其余按值捕获
初始化捕获 [y = 42] C++14 起支持,捕获一个表达式的结果
注意:引用捕获要特别小心生命周期。我曾经在项目里写过一段代码,Lambda 里捕获了一个局部变量的引用,结果 Lambda 被延迟执行时,那个变量已经销毁了。嗯,调试了整整一个下午。

7.3 参数列表与返回类型

参数列表跟普通函数一样,支持默认参数、模板参数(C++20)。返回类型可以省略,让编译器自动推导。

// 自动推导返回类型
auto lambda1 = [](int a, int b) { return a + b; };

// 显式指定返回类型
auto lambda2 = [](int a, int b) -> double { return a / b; };

// 无参数时可以省略括号
auto lambda3 = []{ return 42; };

我个人习惯:如果函数体只有一行 return,就让编译器推导。如果逻辑复杂,我会显式写出返回类型。这样代码更清晰,也避免推导出意外的类型。

7.4 函数体与 mutable 关键字

默认情况下,值捕获的变量在 Lambda 内部是 const 的。你不能修改它们。为什么?因为 Lambda 生成的函数对象,其 operator() 默认是 const 的。

如果你非要修改值捕获的变量,加个 mutable 就行:

int count = 0;
auto counter = [count]() mutable {
    return ++count;
};

std::cout << counter(); // 输出 1
std::cout << counter(); // 输出 2
std::cout << count;     // 输出 0,外部不受影响
小技巧:mutable 只影响值捕获的副本。引用捕获的变量本来就能修改,不需要 mutable。我刚开始学的时候搞混过,后来记住一句话:「值捕获是只读的,想写就加 mutable」。

7.5 知识结构图

下面这张图把 Lambda 基础语法的核心脉络梳理了一遍。你可以把它当作一个快速索引。

Lambda 基础语法 捕获列表 值捕获 [x] 引用捕获 [&x] 隐式捕获 [=] [&] 初始化捕获 [y=42] 参数列表 支持默认参数 C++20 支持模板参数 无参数可省略 () 返回类型 可自动推导 -> 类型 显式指定 void 可省略 函数体 { } 默认 const(值捕获不可修改) 加 mutable 可修改值捕获副本 引用捕获不受 const 限制

7.6 完整示例:从简单到复杂

咱们看几个实际例子。这些代码我都跑过,你可以直接复制去验证。

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    // 最简单的 Lambda
    auto greet = []{ std::cout << "Hello\n"; };
    greet();

    // 带参数的 Lambda
    auto add = [](int a, int b) { return a + b; };
    std::cout << add(3, 4) << "\n";  // 7

    // 捕获外部变量
    int multiplier = 10;
    auto times = [multiplier](int x) { return x * multiplier; };
    std::cout << times(5) << "\n";  // 50

    // 引用捕获 + 修改外部变量
    int sum = 0;
    auto accumulator = [&sum](int x) { sum += x; };
    accumulator(10);
    accumulator(20);
    std::cout << sum << "\n";  // 30

    // 混合捕获
    int factor = 2;
    auto compute = [=, &sum](int x) { 
        sum += x * factor; 
    };
    compute(5);
    std::cout << sum << "\n";  // 40

    // 初始化捕获(C++14)
    auto lambda = [value = 100](int x) { return x + value; };
    std::cout << lambda(50) << "\n";  // 150

    return 0;
}

核心要点:

  • 捕获列表决定 Lambda 能「看到」什么
  • 值捕获是副本,引用捕获是别名
  • mutable 只影响值捕获的副本
  • 返回类型能省略就省略,复杂逻辑再显式写

7.7 避坑指南

我总结几个自己踩过的坑,你遇到了可以少走弯路。

  • 引用捕获悬空:Lambda 被存储或延迟执行时,引用捕获的变量可能已经销毁。解决方案:要么用值捕获,要么确保 Lambda 生命周期不超过变量。
  • 值捕获的误解:值捕获是在 Lambda 定义时拷贝的。如果你在定义后修改了外部变量,Lambda 内部的值不会变。这其实是预期行为,但新手容易搞混。
  • 隐式捕获的副作用[=][&] 很方便,但会捕获所有用到的变量。代码维护时,新增一个变量可能无意中被捕获,导致性能或语义问题。我建议显式列出要捕获的变量。

嗯,Lambda 基础语法就这些。说白了就是四个部分,但每个部分都有讲究。你写多了自然就熟练了。


公众号:蓝海资料掘金营,微信deep3321