C++ Lambda 从入门到实战
C++ Lambda 从入门到实战
别急着复制代码,先搞清楚 Lambda 到底是什么、怎么用、为什么用
前言
如果你写过 C++,大概率遇到过这种场景:想给 std::sort 传个自定义比较规则,结果得专门写个结构体重载 operator();想在一个函数里临时搞个回调,又得去类外面定义一个全局函数。
C++11 引入的 Lambda 表达式 就是为了干掉这种“为了一碟醋包一顿饺子”的写法。
这篇文章我会从最基础的语法讲起,然后深入到捕获方式、泛型 Lambda、可变修饰,最后把那些面试常问、实际踩坑的问题一次性说清楚。
一、Lambda 长什么样
标准语法长这样:
[capture](parameters) -> return_type { body }
其中 -> return_type 可以省略(编译器能自动推导时),parameters 也可以为空(但括号不能省,除非是 C++14 的泛型形式)。
最简单的 Hello Lambda:
auto f = []() { std::cout << "hello lambda\n"; };
f(); // 输出
如果参数和返回值都很简单,可以写成:
auto add = [](int a, int b) { return a + b; }; // 返回值推导为 int
std::cout << add(3, 4); // 7
二、捕获列表:这是 Lambda 的灵魂
Lambda 最强大的地方在于可以捕获外部变量。
2.1 值捕获 [=]
int x = 10;
auto f = [=]() { std::cout << x << '\n'; }; // 拷贝一份 x
x = 20;
f(); // 输出 10,因为捕获的是副本
2.2 引用捕获 [&]
int x = 10;
auto f = [&]() { std::cout << x << '\n'; };
x = 20;
f(); // 输出 20,引用的是同一个变量
2.3 显式指定
int a = 1, b = 2;
auto f = [a, &b]() { /* 可以读 a(值),可以读写 b(引用) */ };
2.4 混合使用
int a = 1, b = 2, c = 3;
auto f = [=, &b]() { /* a 和 c 值捕获,b 引用捕获 */ };
auto g = [&, a]() { /* b 和 c 引用捕获,a 值捕获 */ };
⚠️ 警告:
[=]和[&]在 C++20 之前是“捕获所有”,但如果你在类成员函数里用,要小心this的捕获,后面会讲。
三、mutable:让值捕获也能改副本
默认情况下,值捕获的变量在 Lambda 体内是只读的(因为 operator() 默认是 const)。如果你需要修改副本,加 mutable。
int count = 0;
auto f = [count]() mutable {
count++;
std::cout << count << '\n';
};
f(); // 1
f(); // 2
std::cout << count; // 0,外部原值不变
注意:mutable 必须写在参数列表后面,返回值类型前面(如果有的话)。
四、返回类型推导的坑
大多数情况下编译器能推导返回类型,但如果 body 里有多个 return 且类型不一致,或者你想显式指定,就用尾置返回类型。
auto f = [](int x) -> double {
if (x > 0) return 1.0;
else return 0; // 0 会隐式转为 double
};
不显式指定的话,下面这种会编译报错(C++11 中):
auto bad = [](int x) {
if (x) return 1; // int
else return 1.0; // double,推导冲突
};
// C++14 之后可能按第一个推导,但最好别依赖
五、泛型 Lambda(C++14 起)
如果你想让 Lambda 接受任意类型,参数用 auto:
auto add_any = [](auto a, auto b) { return a + b; };
std::cout << add_any(3, 5); // int
std::cout << add_any(1.2, 3.4); // double
std::cout << add_any(std::string("a"), std::string("b")); // string
这本质上是生成了一个模板化的 operator(),非常灵活。
六、捕获 this 和 *this
在类成员函数里写 Lambda,如果要用到成员变量,捕获 this。
class Foo {
int m_val = 42;
public:
void bar() {
auto f = [this]() { std::cout << m_val; }; // 捕获 this 指针
f();
}
};
但这里有个隐患:如果 Lambda 被延迟执行(比如存到回调里),而 this 对象已经被销毁,那就悬垂指针了。
C++17 提供了 [*this],按值拷贝整个对象:
auto f = [*this]() { std::cout << m_val; }; // 拷贝副本,安全但开销可能大
七、Lambda 的本质是什么
编译器会把 Lambda 转换为一个匿名的函数对象(仿函数),类型是唯一的闭包类型(closure type)。
比如:
auto f = [x](int y) { return x + y; };
大致等价于:
class __AnonymousLambda {
int x;
public:
__AnonymousLambda(int _x) : x(_x) {}
auto operator()(int y) const { return x + y; }
};
auto f = __AnonymousLambda(x);
这就是为什么:
- 每个 Lambda 类型不同,哪怕长得一样
- 值捕获的变量作为成员变量,
mutable控制operator()是否 const
八、什么时候用 std::function,什么时候用 auto
- 用
auto:绝大多数场景,性能最优,无类型擦除开销。 - 用
std::function:需要存储不同类型的可调用对象(比如放到容器里,或者作为多态回调)。
std::function<int(int)> func = [](int x) { return x * 2; };
但 std::function 会有堆分配和虚函数调用开销,能用 auto 就别用 function。
九、常见“翻车”问题
9.1 捕获局部变量的引用,但 Lambda 生命周期超过局部变量
std::function<int()> make_func() {
int local = 100;
return [&]() { return local; }; // 危险!
}
auto f = make_func();
std::cout << f(); // 未定义行为
解决:改成 [=] 或者 [local]。
9.2 在循环里捕获迭代变量
std::vector<std::function<void()>> tasks;
for (int i = 0; i < 3; ++i) {
tasks.push_back([&]() { std::cout << i; }); // 全是同一个 i 的引用
}
for (auto& t : tasks) t(); // 可能输出 333 或其它
解决:捕获副本 [i] 或者 [=](确保 i 是按值)。
9.3 默认 [=] 捕获 this 的陷阱
在成员函数中,[=] 会隐式捕获 this(C++20 之前),你以为捕获了成员变量的值,实际上捕获的是指针。
class Foo {
int x = 10;
public:
void go() {
auto f = [=]() { std::cout << x; }; // 捕获的是 this,不是 x 的副本
// 如果对象销毁,调用 f 就崩
}
};
C++20 废弃了 [=] 隐式捕获 this,建议显式写 [this] 或 [*this]。
十、完整示例代码
下面这个 demo 覆盖了值捕获、引用捕获、mutable、泛型、返回值推导、以及安全使用场景,可以直接跑。
#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include<algorithm>
// 1. 基础:比较大小,用于 sort
void demo_sort() {
std::vector<int> nums = {4, 2, 5, 1, 3};
// 值捕获外部阈值,按降序比较
int threshold = 3;
std::sort(nums.begin(), nums.end(), [threshold](int a, int b) {
return a > b; // 降序
});
std::cout << "Sorted desc: ";
for (int n : nums) std::cout << n << " ";
std::cout << "\n";
}
// 2. mutable:计数器
void demo_mutable() {
int base = 10;
auto counter = [base]() mutable {
static int call_count = 0; // 注意 static 在 lambda 内部是跨调用的
base += 1;
return base;
};
std::cout << "mutable demo: " << counter() << ", " << counter() << "\n"; // 11, 12
std::cout << "base outside: " << base << "\n"; // 10
}
// 3. 泛型 lambda + 返回值推导
void demo_generic() {
auto concat_or_add = [](auto a, auto b) {
return a + b; // 如果 a,b 是 string,就拼接;是数值就相加
};
std::cout << "generic (int): " << concat_or_add(3, 4) << "\n";
std::cout << "generic (string): " << concat_or_add(std::string("Hello "), "World") << "\n";
}
// 4. 避免悬垂引用:正确的捕获方式
std::function<int()> make_safe_func(int value) {
// 按值捕获 value,安全
return [value]() { return value * 2; };
}
void demo_safety() {
auto f = make_safe_func(21);
std::cout << "safe func: " << f() << "\n"; // 42
}
// 5. 成员函数中的 this 捕获(演示安全拷贝)
class Widget {
int data = 100;
public:
void run() {
// 安全:拷贝整个对象(C++17)
auto f = [*this]() mutable {
data += 10; // 修改的是副本
std::cout << "inside lambda: " << data << "\n";
};
f();
std::cout << "original data: " << data << "\n"; // 100
}
};
int main() {
demo_sort();
demo_mutable();
demo_generic();
demo_safety();
Widget w;
w.run();
return 0;
}
运行结果示例:
Sorted desc: 5 4 3 2 1
mutable demo: 11, 12
base outside: 10
generic (int): 7
generic (string): Hello World
safe func: 42
inside lambda: 110
original data: 100
十一、总结
| 场景 | 推荐写法 |
|---|---|
| 临时排序 / 算法回调 | [] 或 [&] 按需捕获,优先 auto 存储 |
| 需要修改捕获的副本 | 加 mutable |
| 异步回调 / 存储延迟执行 | 用 [=] 或显式值捕获,避免引用悬垂 |
| 成员函数中安全捕获 | 用 [*this](C++17)或显式 [this] 并确保生命周期 |
| 需要多态存储 | 使用 std::function,但注意性能损耗 |
| 泛型代码 | 使用 auto 参数(C++14) |
Lambda 不是什么黑魔法,它只是语法糖,但用好了能让代码简洁直观,用错了就是隐晦 bug 的温床。核心记住一点:捕获的是引用还是副本,生命周期是否安全。 其余的都是细枝末节。
如果这篇文章帮到了你,欢迎点赞收藏,有问题评论区见。
更多推荐



所有评论(0)