Lambda 表达式
本节解决什么问题
很多地方需要传入一小段逻辑:排序规则、过滤条件、按钮回调、定时器回调。旧写法通常有三种:
- 写一个普通函数。
- 写一个函数指针。
- 写一个函数对象,也就是带
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:
constexprLambda、[*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_t | STL 中常用于表示大小、数量和下标的无符号整数类型 |
*it | 取得迭代器 it 当前指向的元素 |
其中 begin() 和 end() 经常一起出现:
names.begin(), names.end()
表示整个 names 的遍历范围。需要注意,end() 并不指向最后一个元素,而是最后一个元素后面的结束位置:
Bob Alice David Charlie [结束位置]
↑ ↑
begin() end()
STL 算法为什么要接收一个“规则函数”?
std::count_if、std::find_if、std::sort 有一个共同特点:
算法自己负责“怎么做”,你提供的函数负责“按照什么规则做”。
例如 std::sort 自己知道排序算法应该怎样移动和比较元素,但是它不知道你希望按照数值大小、字符串长度还是其他规则排序。
同样,std::find_if 自己知道怎样从前往后查找,但是它不知道“什么样的元素才算你要找的元素”。
因此这些算法会接收一个可调用对象,并在运行过程中反复调用它。
在示例 1 中已经看到:
std::count_if(begin, end, rule);
它会对每个元素调用:
rule(element);
第三个参数回答的是:
这个元素算不算?
find_if 和 sort 使用的是同一种设计思想,只是它们向规则函数提出的问题不同。
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
一旦第一次得到 true,find_if 就停止查找,并返回当前元素的位置。
因此 find_if 的第三个参数回答的是:
当前元素是不是我要找的?
它和 count_if 的规则函数形式很像,通常都是:
bool rule(一个元素);
区别在于算法得到 true 之后做什么:
| 算法 | 规则返回 true 后的行为 |
|---|---|
count_if | 计数加一,然后继续检查后面的元素 |
find_if | 已经找到,立即停止并返回当前位置 |
std::sort:a 应不应该排在 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;
}
就再使用字符串的字典顺序决定谁在前面。例如 Alice 和 David 长度都是 5,因此比较:
"Alice" < "David"
结果为 true,所以 Alice 排在 David 前面。
三个算法可以这样记:
| API | 第三个参数回答什么? |
|---|---|
count_if | 这个元素算不算? |
find_if | 这个元素是不是我要找的? |
sort | a 应不应该排在 b 前面? |
这正是 Lambda 很常见的用途:STL 算法负责通用流程,Lambda 负责描述具体规则。
const std::string& 和 .size()
Lambda 中的:
const std::string& a
可以拆成:
std::string:参数是字符串;&:使用引用,不额外复制整个字符串;const:只读取这个字符串,不修改它。
因此 const std::string& 常用于只读地接收 string、vector 等较大的对象。
字符串的:
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_length 是 int。转换以后,两边使用适合的同类整数类型进行比较。
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 作为排序规则:
- 长度不同,短的排前面。
- 长度相同,按照字符串字典顺序排列。
所以得到:
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 用按值捕获,因为离开内部作用域后,name 和 score 已经销毁。
示例 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] 引用 |
| 示例 4 | mutable | 修改的是 lambda 内部副本,不影响外部变量 |
| 示例 5 | STL 算法 | sort、find_if、count_if 常和 lambda 配合 |
| 示例 6 | 生命周期 | 保存回调、异步回调、线程回调中不要随便引用捕获局部变量 |
| 示例 7 | [this] | 捕获当前对象指针,Lambda 访问的是原对象 |
| 示例 8 | [*this] | C++17 起捕获当前对象副本,与原对象状态分离 |
常见错误
- 保存回调时使用默认引用捕获
[&],导致局部变量销毁后仍被访问。 - 以为按值捕获会跟着外部变量变化。按值捕获保存的是定义 lambda 时的副本。
- 想修改按值捕获的副本,却忘记加
mutable。 - 把有捕获的 lambda 当成函数指针使用。有捕获的 lambda 需要用模板参数、
auto变量或std::function保存。 - 异步场景捕获
this后,对象先被销毁。需要保证对象生命周期,或使用智能指针、[*this]等更明确的方式。
使用建议
- 小而局部的逻辑优先用 lambda。
- 捕获列表尽量显式写
[x, &y],少用默认[=]和[&]。 - 保存回调、线程、定时器、异步操作中优先按值捕获需要的数据。
- 只调用一次、不需要保存的回调,可以直接把 lambda 传给算法或函数模板。
- 类成员函数中的回调使用
[this]时要保证对象生命周期;需要独立保存对象状态时可以考虑 C++17 的[*this]。 - 需要统一保存不同 lambda 时,再使用下一节的
std::function。
小结
- Lambda 是匿名可调用对象,写法是
[捕获](参数) { 函数体 }。 - 捕获列表决定 lambda 如何使用外部变量。
[x]是按值捕获,[&x]是按引用捕获。mutable允许修改按值捕获的内部副本。[this]捕获当前对象指针,[*this]从 C++17 起可以捕获当前对象副本。- Lambda 最常用于 STL 算法、回调和异步任务。
- 生命周期是 Lambda 最容易出错的地方,尤其是保存回调和异步回调。