有一部分在第四章的讲解里面,我再单独为lambda开一章,因为发现这里也有很多可以讲的


✅ Lambda 捕获的本质

lambda 捕获是把外部作用域的变量复制进 lambda 的闭包对象(closure object)里

你可以把 lambda 看成一个“生成临时类的语法糖”:

1
auto f = [x]() { return x + 1; };

其实等价于生成一个类:

1
2
3
4
struct __Lambda {
int x; // 捕获变量
int operator()() { return x + 1; }
};

✅ 一、按值捕获 [x]

含义:

把外部变量 拷贝 一份到 lambda 内部。

例子:

1
2
3
4
int a = 10;
auto f = [a]() { return a; };
a = 20;
cout << f(); // 输出 10

📌 特点:

  • captured 值不会随外面变化
  • 内部只看到“值的快照”
  • 拷贝副本,因此线程安全、无悬挂引用

✅ 二、引用捕获 [&x]

含义:

lambda 内部保存的是 外部变量的引用,不拷贝。

例子:

1
2
3
4
int a = 10;
auto f = [&a]() { return a; };
a = 20;
cout << f(); // 输出 20

📌 特点:

  • 外部的变化会反映进 lambda
  • 危险:如果外部变量生命周期结束,则引用悬空!

✅ 三、隐式捕获(最常用)

[=] 按值捕获所有用到的外部变量

1
2
int x = 1, y = 2;
auto f = [=]() { return x + y; };

[&] 引用捕获所有用到的外部变量

1
2
int x = 1, y = 2;
auto f = [&]() { x = 3; y = 4; };

✅ 四、混合捕获

可以混着写:

[=, &x]

默认按值捕获,但 x 用引用捕获。

1
2
3
4
5
int x=1, y=2;
auto f = [=, &x]() {
x = 10; // 修改 x
return y; // y 是值捕获
};

[&, x]

默认引用捕获,但 x 是按值捕获。


✅ 五、捕获 this / 捕获 *this

[this]

捕获当前对象的指针(隐式捕获所有成员)。

1
2
3
4
5
6
7
class A {
int x = 10;
public:
auto func() {
return [this]() { return x; };
}
};

[=*this](C++17)

深拷贝整个对象,捕获 this 指向的内容,而不是指针。


✅ 六、初始化捕获(C++14 起)

最强大、也是你当前线程池代码用到的:

[name = expr]

把任意表达式的结果绑定到捕获变量。

例如把参数 move 进 lambda:

1
2
3
auto f = [p = std::move(vec)]() {
return p.size();
};

这个语法就是:

  • 创建一个成员变量 p
  • 初始化为 std::move(vec)
  • lambda 里使用的是内部的 p,不是外面的 vec

你的代码中就是用它来“提前绑定 f 和 args”:

1
[func = std::forward<F>(f), ... captured = std::forward<Args>(args)]()

✅ 七、结构化绑定捕获(C++20)

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

✅ 八、完整捕获表总结

捕获方式 含义 是否拷贝 安全性
[x] 按值 ✅ 安全
[&x] 引用 ⚠️ 外部变量不能提前析构
[=] 按值捕获所有用到的变量
[&] 引用捕获所有用到的变量 ⚠️
[=, &x] 默认值捕获,x引用捕获 混合 依情况
[&, x] 默认引用,x 按值 混合 依情况
[this] 捕获 this 指针 ⚠️ 成员变量可能悬空
[=*this] 深拷贝当前对象
[var = expr] 初始化捕获 ✅ 最强大
[a, b, c] 精确指定按值捕获

✅ 九、结合你线程池代码解释(非常重要)

你问的这一行:

1
tasks.emplace([task](){ (*task)(); });

这里的 [task] 是按值捕获
task 是一个 std::shared_ptr<packaged_task<...>>

✅ 按值捕获 shared_ptr = 拷贝 shared_ptr(引用计数 +1)
✅ lambda 内部再调用 (*task)();
✅ 这个 lambda 变成 std::function<void()> 放入队列
✅ worker 执行队列时执行 task->operator()()
✅ 任务结束后未来值写进 future

这正是正确的做法,因为:

  • 保证 packaged_task 的生命周期
  • 避免绑定参数被拷贝多次
  • 避免悬空指针
  • lambda 必须是无参(绑定参数已提前捕获)

✅ 十、一句话总结

按值捕获 = 拷贝变量到 lambda 内

引用捕获 = 捕获外部变量的引用

初始化捕获([x=expr])= 构造一个“新局部变量 x”存入 lambda,为对象绑定参数最常用

lambda 存入 std::function 时会复制整个闭包对象,包括捕获的内容


注意捕获列表里面的,只能是已有的参数,不能是新的参数,初始化捕获除外(但其等号右边也必须是上下文已经有了的参数)

初始化捕获用auto机制,解决了move的问题,减少了拷贝

前面我们提到bind和lambda都能实现参数的绑定,但实际上lambda在某种意义上代替了bind的生态位。

也就是说:

✅ **C++11 之前我们用 std::bind**(因为那时候还没有 lambda)
C++11 之后 lambda 一出来,std::bind 基本就被淘汰了


🧩 一、std::bind 的历史背景

C++98/03 时代,没有 lambda。
如果你想“预先绑定一些参数”,只能用 std::bind1ststd::bind2ndstd::bindstd::mem_fn 等函数适配器。

1
2
3
4
5
6
7
8
9
10
#include <functional>
#include <iostream>
using namespace std;

int add(int a, int b) { return a + b; }

int main() {
auto add5 = std::bind(add, 5, std::placeholders::_1);
cout << add5(3); // 输出 8
}

这相当于创建了一个新的函数对象:
add5(x) = add(5, x)


🧠 二、lambda 出现后做了什么

C++11 引入 lambda 表达式,可以直接写出:

1
auto add5 = [=](int x){ return add(5, x); };

完全等价于上面的 std::bind(add, 5, _1),而且:

  • 更直观
  • 能看出参数的名字和顺序
  • 支持完美转发
  • 调试友好(不会生成神秘模板类型)

所以现代写法几乎都变成了:

原来的 bind 写法 现代 lambda 写法
std::bind(add, 5, _1) [=](auto x){ return add(5, x); }
std::bind(&X::f, &obj, _1) [&](auto x){ obj.f(x); }
std::bind(&X::set, &obj, 42) [&]{ obj.set(42); }

🧩 三、为什么 lambda 能替代 bind?

对比点 std::bind lambda
引入版本 C++98 C++11
可读性 差(_1, _2 模糊) 高(参数清晰)
类型安全 不安全(模板推断复杂) 完美类型推断
性能 可能多层包装 编译期内联优化
调试体验 难读难看 直接看 lambda 逻辑
完美转发支持 ❌ 不支持 ✅ 支持(std::forward
捕获外部变量 ❌ 不能 ✅ 能捕获(按值、引用、自定义)

🔹 最大区别:
std::bind 只能绑定参数,不能捕获外部变量。
而 lambda 可以随意捕获任意作用域内的对象。


🧩 四、一个实际对比示例

🏷 用 bind 的旧写法(C++03 / 早期 C++11)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <functional>
#include <iostream>
using namespace std;

struct Worker {
void run(int id) { cout << "Run " << id << endl; }
};

int main() {
Worker w;
auto task = std::bind(&Worker::run, &w, 42);
task(); // 输出 Run 42
}

🏷 现代 lambda 写法

1
2
3
Worker w;
auto task = [&]() { w.run(42); };
task();

看出来了吗?

  • std::bind 语法啰嗦;
  • 不支持捕获;
  • 编译错误信息又长又难懂;
  • 读起来不如 lambda 直觉。

🧩 五、lambda = 语法层面的 bind 超集

可以认为:

lambda 是“语法层面内置”的 bind + function object + capture 支持

本质等价于:

1
auto lam = [captured = std::move(obj)](int x, int y) { return captured.func(x, y); };

vs

1
auto lam = std::bind(&Class::func, obj, std::placeholders::_1, std::placeholders::_2);

但 lambda:

  • 能 inline 编译
  • 能直接在模板中推断类型
  • 不需要占位符 _1 _2
  • 可以完美转发参数

🧩 六、在你的线程池上下文里

你那段代码:

1
2
3
4
5
auto task = std::make_shared<std::packaged_task<return_type()>>(
[func = std::forward<F>(f), ... captured = std::forward<Args>(args)]() mutable -> return_type {
return std::invoke(func, std::move(captured)...);
}
);

如果没有 lambda,你只能这么写(旧时代的方式):

1
2
auto bound = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
auto task = std::make_shared<std::packaged_task<return_type()>>(bound);

但是:

  • std::bind 会复制所有参数;
  • 无法 move
  • 无法控制捕获方式;
  • 不支持完美转发;
  • 性能可能更差。

所以现代线程池几乎都写成 lambda。


🧩 七、结论总结

项目 std::bind lambda
定义时间 C++98 C++11
功能 绑定参数创建函数对象 绑定外部变量创建函数对象
是否立即调用 ❌ 延迟 ❌ 延迟
语法可读性
性能 一般 高(可内联)
类型推断 强(自动)
捕获变量 ❌ 不支持 ✅ 支持任意方式
是否推荐使用 ❌ 不推荐(现代C++弃用) ✅ 推荐

✅ 一句话总结:

lambda 是现代 C++ 的“语法级 bind”,功能更强、语法更直觉、性能更好。

可以这么说:

std::bind 是旧时代没有 lambda 时的权宜之计,
lambda 是 C++ 正式把“函数式编程”纳入语言级语法的结果。