第 22.11 節

Lambda 表达式

0瀏覽次數0訪問次數--跳出率--平均停留

本节解决什么问题

很多地方需要传入一小段逻辑:排序规则、过滤条件、按钮回调、定时器回调。旧写法通常有三种:

  1. 写一个普通函数。
  2. 写一个函数指针。
  3. 写一个函数对象,也就是带 operator() 的类或结构体。

这些写法都能工作,但小逻辑会被迫挪到远处,或者要额外写一个类型。Lambda 表达式让你可以在使用现场直接写一个小函数,而且可以捕获外部变量。

这个特性是什么

Lambda 是匿名可调用对象。它看起来像一个函数,但本质上是编译器生成的一个类对象。

基本结构是:

部分例子含义
捕获列表[x, &y]从外部作用域拿哪些变量
参数列表(int a, int b)调用 lambda 时传入什么
返回值-> int返回类型,通常可以省略
函数体{ return a + b; }真正执行的代码

完整形式可以写成 [x](int n) -> int { return x + n; },常见情况下返回值类型可以省略。

C++ 标准版本

  • C++11:基础 Lambda。
  • C++14:泛型 Lambda,也就是参数可以写 auto
  • C++17:constexpr Lambda、[*this] 捕获。

Lambda 是语言特性,不需要额外头文件。只有配合 STL 算法、std::function 等库工具时,才需要包含对应头文件。

捕获列表速查

写法含义适用场景
[]不捕获外部变量只用参数或局部临时变量
[x]按值捕获 x保存一份副本,适合保存回调
[&x]按引用捕获 x需要修改外部变量,且能保证生命周期
[x, &y]混合捕获明确表达哪些拷贝、哪些引用
[=]默认按值捕获用到的变量小例子方便,工程中不建议滥用
[&]默认按引用捕获用到的变量异步或保存回调中尤其危险
[this]捕获当前对象指针对象必须比 lambda 活得久
[*this]捕获当前对象副本C++17 起可用,适合避免悬空 this

示例代码

示例 1:旧回调写法和 lambda 的区别

普通函数和函数对象都可以作为算法条件,但 Lambda 更适合写简短的局部逻辑。

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

bool is_even(int n)
{
    return n % 2 == 0;
}

struct GreaterThan
{
    int limit;

    bool operator()(int n) const
    {
        return n > limit;
    }
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // vector 是动态数组,元素数量可以在运行时变化。
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);
    std::cout << "even count = " << even_count << "\n";

    int greater_count1 = std::count_if(numbers.begin(), numbers.end(), GreaterThan{3});
    std::cout << "> 3 count (functor) = " << greater_count1 << "\n";

    int limit = 3;
    int greater_count2 = std::count_if(numbers.begin(), numbers.end(),
                                       [limit](int n) {
                                           return n > limit;
                                       });
    std::cout << "> 3 count (lambda) = " << greater_count2 << "\n";

    return 0;
}

运行结果

even count = 3
> 3 count (functor) = 3
> 3 count (lambda) = 3

count_if 的第三个参数是什么?

先看这一句:

int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);

std::count_if 可以简单理解为:

从指定范围中逐个取出元素,调用你提供的“判断规则”,统计其中返回 true 的元素有多少个。

它的基本形式是:

std::count_if(起点, 终点, 判断规则);

第三个参数就是这个“判断规则”。它并不一定非得是普通函数,只要是可以像函数一样调用的对象就可以,包括普通函数、函数对象和 Lambda。这些东西统称为可调用对象(Callable)

这里第三个参数传入的是:

is_even

is_even 是:

bool is_even(int n)
{
    return n % 2 == 0;
}

count_if 会把 numbers 中的元素一个一个传给它,过程可以近似理解成:

is_even(1);  // false,不计数
is_even(2);  // true,计数 +1
is_even(3);  // false,不计数
is_even(4);  // true,计数 +1
is_even(5);  // false,不计数
is_even(6);  // true,计数 +1

因此最后得到 3

可以把 count_if 的内部工作近似理解成:

int count = 0;

for (每一个元素)
{
    if (判断规则(当前元素))
    {
        ++count;
    }
}

也就是说,count_if 自己负责:

  • 怎么遍历容器;
  • 怎么统计数量。

第三个参数负责告诉它:

什么样的元素才算符合条件?

同一个位置也可以传入函数对象:

std::count_if(numbers.begin(), numbers.end(), GreaterThan{3});

GreaterThan{3} 可以像函数一样被调用,因为结构体定义了:

bool operator()(int n) const
{
    return n > limit;
}

它回答的问题就是:

当前这个 n 是否大于 3

还可以直接传 Lambda:

std::count_if(numbers.begin(), numbers.end(),
              [limit](int n) {
                  return n > limit;
              });

count_if 同样会反复调用这个 Lambda:

lambda(1);  // false
lambda(2);  // false
lambda(3);  // false
lambda(4);  // true
lambda(5);  // true
lambda(6);  // true

所以这三种写法虽然形式不同,本质上都是在给 count_if 提供一个判断规则:

is_even
GreaterThan{3}
[limit](int n) { return n > limit; }

Lambda 的优势在于:如果这个判断规则只在这里使用一次,就可以直接写在算法旁边,不需要额外定义一个函数或结构体。

示例 2:最基本的 lambda 语法、参数和返回值

#include <iostream>
#include <string>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto add = [](int a, int b) {
        return a + b;
    };

    auto describe_score = [](int score) -> std::string {
        if (score >= 60)
        {
            return "pass";
        }
        return "fail";
    };

    std::cout << "add(3, 5) = " << add(3, 5) << "\n";
    std::cout << "score 80 is " << describe_score(80) << "\n";
    std::cout << "score 40 is " << describe_score(40) << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果

add(3, 5) = 8
score 80 is pass
score 40 is fail

示例 3:按值捕获和按引用捕获

按值捕获会保存定义 lambda 时的副本;按引用捕获会访问外部变量本身。

#include <iostream>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int score = 10;

    auto add_by_value = [score](int bonus) {
        return score + bonus;
    };

    auto add_by_ref = [&score](int bonus) {
        score += bonus;
        return score;
    };

    score = 20;

    std::cout << "value capture result = " << add_by_value(5) << "\n";
    std::cout << "ref capture result = " << add_by_ref(5) << "\n";
    std::cout << "score after ref capture = " << score << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果

value capture result = 15
ref capture result = 25
score after ref capture = 25

示例 4:mutable 允许修改按值捕获的副本

按值捕获的变量默认在 lambda 内是只读的。加上 mutable 后,可以修改 lambda 自己保存的副本,但不会修改外部变量。

#include <iostream>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int start = 0;

    auto counter = [start]() mutable {
        ++start;
        return start;
    };

    std::cout << "counter() = " << counter() << "\n";
    std::cout << "counter() = " << counter() << "\n";
    std::cout << "outside start = " << start << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

运行结果

counter() = 1
counter() = 2
outside start = 0

为什么连续调用 counter() 会继续累加?

这里不是因为外部 start 的作用域变大了,而是因为 Lambda 对象 counter 自己保存了一份 start

int start = 0;

auto counter = [start]() mutable {
    ++start;
    return start;
};

[start] 是按值捕获。可以近似理解为编译器生成了这样的对象:

struct Counter
{
    int start;

    int operator()()
    {
        ++start;
        return start;
    }
};

Counter counter{start};

因此程序中实际上有两个不同的 start

  • 外部 start:始终还是 0
  • counter 内部保存的 start:第一次调用变成 1,第二次调用继续变成 2
counter();  // 内部 start:0 -> 1
counter();  // 内部 start:1 -> 2

关键在于:

auto counter = [start]() mutable { ... };

这里只创建了一次 Lambda 对象。后面的两个 counter() 都是在调用同一个对象,不会每次调用时重新捕获一次外部 start

mutable 的作用则是允许修改 Lambda 内部按值捕获的副本,它并不会让外部的 start 一起变化。

示例 5:lambda 配合 STL 算法

Lambda 和 STL 算法配合时最常见:排序、查找、计数、转换都可以把局部逻辑直接写在调用处。

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

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // vector 是动态数组,元素数量可以在运行时变化。
    std::vector<std::string> names = {"Bob", "Alice", "Charlie", "David"};

    std::sort(names.begin(), names.end(),
              [](const std::string& a, const std::string& b) {
                  if (a.size() == b.size())
                  {
                      return a < b;
                  }
                  return a.size() < b.size();
              });

    std::cout << "sort by length: ";
    for (const auto& name : names)
    {
        std::cout << name << " ";
    }
    std::cout << "\n";

    int min_length = 6;
    auto it = std::find_if(names.begin(), names.end(),
                           [min_length](const std::string& name) {
                               return name.size() >= static_cast<std::size_t>(min_length);
                           });

    if (it != names.end())
    {
        std::cout << "first long name = " << *it << "\n";
    }

    return 0;
}

运行结果

sort by length: Bob Alice David Charlie
first long name = Charlie

示例中使用的 API 和语法

这个示例除了 Lambda,还使用了一些常见的 STL API。

写法简单说明
std::vector<std::string>动态数组,这里用于保存多个字符串
names.begin()返回指向第一个元素的迭代器
names.end()返回最后一个元素后一个位置的迭代器
std::sort(begin, end, rule)对指定范围排序,第三个参数指定比较规则
string.size()返回字符串长度
const std::string&只读引用字符串,避免额外复制
for (const auto& x : container)范围 for,依次访问容器中的每个元素
auto让编译器自动推导变量类型
std::find_if(begin, end, rule)查找第一个满足条件的元素
static_cast<T>(value)value 显式转换为类型 T
std::size_tSTL 中常用于表示大小、数量和下标的无符号整数类型
*it取得迭代器 it 当前指向的元素

其中 begin()end() 经常一起出现:

names.begin(), names.end()

表示整个 names 的遍历范围。需要注意,end() 并不指向最后一个元素,而是最后一个元素后面的结束位置:

Bob    Alice    David    Charlie    [结束位置]
 ↑                                      ↑
begin()                                end()

STL 算法为什么要接收一个“规则函数”?

std::count_ifstd::find_ifstd::sort 有一个共同特点:

算法自己负责“怎么做”,你提供的函数负责“按照什么规则做”。

例如 std::sort 自己知道排序算法应该怎样移动和比较元素,但是它不知道你希望按照数值大小、字符串长度还是其他规则排序。

同样,std::find_if 自己知道怎样从前往后查找,但是它不知道“什么样的元素才算你要找的元素”。

因此这些算法会接收一个可调用对象,并在运行过程中反复调用它。

在示例 1 中已经看到:

std::count_if(begin, end, rule);

它会对每个元素调用:

rule(element);

第三个参数回答的是:

这个元素算不算?

find_ifsort 使用的是同一种设计思想,只是它们向规则函数提出的问题不同。

std::find_if:这个元素是不是我要找的?

基本形式是:

std::find_if(起点, 终点, 判断规则);

示例中:

auto it = std::find_if(names.begin(), names.end(),
                       [min_length](const std::string& name) {
                           return name.size() >= static_cast<std::size_t>(min_length);
                       });

[min_length] 按值捕获最小长度 6。这个 Lambda 接收一个字符串,并判断它的长度是否至少为 6

find_if 会依次调用这个 Lambda,可以近似理解成:

rule("Bob");      // false
rule("Alice");    // false
rule("David");    // false
rule("Charlie");  // true

一旦第一次得到 truefind_if 就停止查找,并返回当前元素的位置。

因此 find_if 的第三个参数回答的是:

当前元素是不是我要找的?

它和 count_if 的规则函数形式很像,通常都是:

bool rule(一个元素);

区别在于算法得到 true 之后做什么:

算法规则返回 true 后的行为
count_if计数加一,然后继续检查后面的元素
find_if已经找到,立即停止并返回当前位置

std::sorta 应不应该排在 b 前面?

std::sort 稍微不同:

std::sort(起点, 终点, 比较规则);

它的第三个参数一次接收两个元素

[](const std::string& a, const std::string& b) {
    if (a.size() == b.size())
    {
        return a < b;
    }
    return a.size() < b.size();
}

sort 会在排序过程中不断调用:

rule(a, b);

这个函数要回答:

按照当前排序规则,a 是否应该排在 b 前面?

例如:

rule("Bob", "Charlie");

因为:

Bob      长度 3
Charlie  长度 7

最终判断的是:

3 < 7

结果为 true,表示按照当前规则,Bob 应该排在 Charlie 前面。

如果两个字符串长度相同:

if (a.size() == b.size())
{
    return a < b;
}

就再使用字符串的字典顺序决定谁在前面。例如 AliceDavid 长度都是 5,因此比较:

"Alice" < "David"

结果为 true,所以 Alice 排在 David 前面。

三个算法可以这样记:

API第三个参数回答什么?
count_if这个元素算不算?
find_if这个元素是不是我要找的?
sorta 应不应该排在 b 前面?

这正是 Lambda 很常见的用途:STL 算法负责通用流程,Lambda 负责描述具体规则。

const std::string&.size()

Lambda 中的:

const std::string& a

可以拆成:

  • std::string:参数是字符串;
  • &:使用引用,不额外复制整个字符串;
  • const:只读取这个字符串,不修改它。

因此 const std::string& 常用于只读地接收 stringvector 等较大的对象。

字符串的:

a.size()

返回字符串长度。例如:

"Bob"       -> 3
"Alice"     -> 5
"Charlie"   -> 7
"David"     -> 5

所以示例的排序结果是:

Bob Alice David Charlie

范围 for

for (const auto& name : names)
{
    std::cout << name << " ";
}

可以直接理解为:

依次取出 names 中的每个元素,并把当前元素命名为 name

这里的:

const auto& name

编译器会根据 names 的元素类型自动推导,近似等价于:

const std::string& name

static_cast<std::size_t>

这里:

static_cast<std::size_t>(min_length)

是显式类型转换,可以简单理解为:

min_length 转换成 std::size_t 类型。

因为 name.size() 返回的是 std::size_t,而:

int min_length = 6;

中的 min_lengthint。转换以后,两边使用适合的同类整数类型进行比较。

std::size_t 常用于表示容器大小、字符串长度和数组下标等非负数量。

it 和迭代器

auto it = std::find_if(...);

find_if 返回的不是字符串本身,而是一个迭代器

迭代器可以暂时理解成类似指针的对象,用来表示容器中的某个位置。

排序后的数据是:

Bob    Alice    David    Charlie
                         ↑
                         it

因此:

*it

取得的就是:

Charlie

如果 find_if 一直没有找到满足条件的元素,它会返回:

names.end()

所以常见写法是:

if (it != names.end())
{
    std::cout << *it << "\n";
}

意思就是:

如果确实找到了元素,再使用它。

整段代码的执行过程

程序首先创建:

std::vector<std::string> names = {
    "Bob",
    "Alice",
    "Charlie",
    "David"
};

然后 std::sort 使用 Lambda 作为排序规则:

  1. 长度不同,短的排前面。
  2. 长度相同,按照字符串字典顺序排列。

所以得到:

Bob Alice David Charlie

接下来:

int min_length = 6;

规定要寻找长度至少为 6 的名字。

std::find_if 会按照当前顺序依次调用 Lambda:

Bob       -> 3 >= 6 -> false
Alice     -> 5 >= 6 -> false
David     -> 5 >= 6 -> false
Charlie   -> 7 >= 6 -> true

第一次得到 true 时停止,因此 it 最终指向 Charlie

*it

得到:

Charlie

最终输出:

sort by length: Bob Alice David Charlie
first long name = Charlie

这个示例体现了 Lambda 非常典型的一种用途:

STL 算法负责通用流程,Lambda 负责描述具体规则。

示例 6:保存回调时要注意捕获生命周期

如果 lambda 只是立刻调用,引用捕获通常看起来没问题;如果保存到容器、线程、定时器或异步回调里,lambda 可能晚于局部变量执行。保存回调时优先按值捕获需要的数据。

#include <functional>
#include <iostream>
#include <string>
#include <vector>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // std::function 可以保存普通函数、lambda 或函数对象。
    // vector 是动态数组,元素数量可以在运行时变化。
    std::vector<std::function<void()>> callbacks;

    {
        std::string name = "Alice";
        int score = 95;

        auto print_now = [&name, &score]() {
            std::cout << "now: " << name << " " << score << "\n";
        };
        print_now();

        callbacks.push_back([name, score]() {
            std::cout << "saved: " << name << " " << score << "\n";
        });
    }

    for (const auto& callback : callbacks)
    {
        callback();
    }

    return 0;
}

运行结果

now: Alice 95
saved: Alice 95

这里 print_now 立刻调用,所以引用捕获没问题。保存到 callbacks 的 lambda 用按值捕获,因为离开内部作用域后,namescore 已经销毁。

示例 7:捕获 this 访问当前对象

在类的成员函数中,Lambda 可以使用 [this] 捕获当前对象的指针。Lambda 访问的仍然是原来的对象,因此对象成员之后发生的变化也能看到。

#include <iostream>

class Robot
{
public:
    Robot(int speed) : speed_(speed)
    {
    }

    void demo()
    {
        auto print_speed = [this]() {
            std::cout << "lambda speed = " << speed_ << "\n";
        };

        speed_ = 20;
        print_speed();
    }

private:
    int speed_;
};

int main()
{
    Robot robot(10);
    robot.demo();

    return 0;
}

运行结果

lambda speed = 20

这里:

[this]() {
    std::cout << speed_;
}

可以近似理解为 Lambda 保存了当前对象的 this 指针,因此:

speed_

实际上访问的是:

this->speed_

创建 Lambda 时 speed_10,之后原对象把 speed_ 修改成 20。因为 [this] 指向的仍然是原对象,所以调用 Lambda 时读到的是 20

需要特别注意:[this] 保存的是对象指针,不是对象副本。如果 Lambda 被保存下来,而原对象已经销毁,再通过这个 Lambda 访问成员就可能产生悬空指针问题。因此保存回调、异步任务和线程回调中使用 [this] 时要特别关注对象生命周期。

示例 8:使用 [*this] 捕获当前对象副本

C++17 开始可以使用 [*this]。它不是保存当前对象的指针,而是在创建 Lambda 时复制一份当前对象。

#include <iostream>

class Robot
{
public:
    Robot(int speed) : speed_(speed)
    {
    }

    void demo()
    {
        auto print_speed = [*this]() {
            std::cout << "lambda copy speed = " << speed_ << "\n";
        };

        speed_ = 20;

        print_speed();
        std::cout << "outside speed = " << speed_ << "\n";
    }

private:
    int speed_;
};

int main()
{
    Robot robot(10);
    robot.demo();

    return 0;
}

运行结果

lambda copy speed = 10
outside speed = 20

这里 Lambda 创建时:

[*this]

会把当时的 Robot 对象复制一份保存到 Lambda 内部。此时副本中的:

speed_ = 10

之后:

speed_ = 20;

修改的是外面的原对象,不会修改 Lambda 已经保存的对象副本,因此 Lambda 仍然输出 10

可以把两种写法这样对比:

写法Lambda 保存什么原对象后来修改成员后
[this]当前对象的指针Lambda 能看到新的成员值
[*this]当前对象的副本Lambda 保留捕获时的成员值

[*this] 可以避免因为原对象销毁而直接留下一个悬空的 this 指针,但它会复制整个对象,因此也要考虑对象是否适合复制以及复制成本。

如果需要在 Lambda 中修改这份对象副本,与普通按值捕获类似,可以再配合 mutable

auto callback = [*this]() mutable {
    ++speed_;
};

这里修改的仍然只是 Lambda 内部保存的对象副本,不会修改外面的原对象。

关键语法解释

示例重点说明
示例 1旧回调写法对比普通函数、函数对象都能做回调,但 lambda 更适合局部短逻辑
示例 2参数和返回值返回类型通常可推导,分支返回不同类型时要显式写清楚
示例 3捕获列表[x] 拷贝,[&x] 引用
示例 4mutable修改的是 lambda 内部副本,不影响外部变量
示例 5STL 算法sortfind_ifcount_if 常和 lambda 配合
示例 6生命周期保存回调、异步回调、线程回调中不要随便引用捕获局部变量
示例 7[this]捕获当前对象指针,Lambda 访问的是原对象
示例 8[*this]C++17 起捕获当前对象副本,与原对象状态分离

常见错误

  1. 保存回调时使用默认引用捕获 [&],导致局部变量销毁后仍被访问。
  2. 以为按值捕获会跟着外部变量变化。按值捕获保存的是定义 lambda 时的副本。
  3. 想修改按值捕获的副本,却忘记加 mutable
  4. 把有捕获的 lambda 当成函数指针使用。有捕获的 lambda 需要用模板参数、auto 变量或 std::function 保存。
  5. 异步场景捕获 this 后,对象先被销毁。需要保证对象生命周期,或使用智能指针、[*this] 等更明确的方式。

使用建议

  1. 小而局部的逻辑优先用 lambda。
  2. 捕获列表尽量显式写 [x, &y],少用默认 [=][&]
  3. 保存回调、线程、定时器、异步操作中优先按值捕获需要的数据。
  4. 只调用一次、不需要保存的回调,可以直接把 lambda 传给算法或函数模板。
  5. 类成员函数中的回调使用 [this] 时要保证对象生命周期;需要独立保存对象状态时可以考虑 C++17 的 [*this]
  6. 需要统一保存不同 lambda 时,再使用下一节的 std::function

小结

  • Lambda 是匿名可调用对象,写法是 [捕获](参数) { 函数体 }
  • 捕获列表决定 lambda 如何使用外部变量。
  • [x] 是按值捕获,[&x] 是按引用捕获。
  • mutable 允许修改按值捕获的内部副本。
  • [this] 捕获当前对象指针,[*this] 从 C++17 起可以捕获当前对象副本。
  • Lambda 最常用于 STL 算法、回调和异步任务。
  • 生命周期是 Lambda 最容易出错的地方,尤其是保存回调和异步回调。
音乐页