std::promise::set_exception必须用std::current_exception(),否则future::get()抛broken_promise而非原始异常;promise析构前须调用set_value或set_exception;packaged_task自动处理异常传递,裸promise需手动调用。

std::promise::set_exception 为什么必须用 std::current_exception()
直接 throw 异常不会自动传递到 std::future 端,std::promise 不捕获当前异常对象——它只接受一个 std::exception_ptr。漏掉 std::current_exception() 是最常见的错误,结果是调用线程调用 get() 时抛出 std::future_error(错误码为 std::future_errc::broken_promise),而不是你预期的原始异常。
正确做法是:在 catch 块中用 std::current_exception() 捕获当前异常上下文,再传给 set_exception():
std::promise<int> prom;
std::thread t([&prom] {
try {
throw std::runtime_error("oops");
} catch (...) {
prom.set_exception(std::current_exception()); // ✅ 关键
}
});
t.join();
try {
prom.get(); // 这里会 rethrow runtime_error
} catch (const std::runtime_error& e) {
// 处理
}
std::promise 被销毁前必须 set_value 或 set_exception
如果 promise 对象析构时既没调用 set_value() 也没调用 set_exception(),其关联的 std::future 在 get() 时会抛出 std::future_error(broken_promise)。这和“是否发生异常”无关,只和 promise 是否被显式完成有关。
- 未完成的 promise 可能出现在提前 return、未覆盖的分支、或异常中途跳出的场景中
- 建议在关键路径上用 RAII 封装,例如用 lambda +
std::shared_ptr管理 promise 生命周期,或在函数出口统一检查 - 不要依赖析构函数自动清理——
std::promise析构不触发任何异常传递逻辑
跨线程传递异常类型信息会丢失吗?
不会。通过 std::current_exception() 获取的 std::exception_ptr 保留了原始异常的完整类型和 what() 内容,只要目标线程有该异常类型的定义(即头文件已包含、RTTI 开启),catch 就能按原类型捕获。
立即学习“C++免费学习笔记(深入)”;
但要注意:
- 如果异常类型只在生产方线程的编译单元中定义(比如匿名命名空间里的类),消费方线程无法识别该类型,会 fallback 到
std::exception或更通用基类 - 确保两端都链接了同一份异常类型定义;避免在头文件里用
static或匿名 namespace 定义异常类 - RTTI 必须启用(GCC/Clang 默认开启,MSVC 需确认未加
/GR-)
std::promise 和 std::packaged_task 在异常传递上的差异
std::packaged_task 本质封装了 promise + 可调用对象,它在执行任务函数时**自动处理异常**:若任务抛异常,会隐式调用 set_exception(std::current_exception());而裸 std::promise 什么也不做,全靠手动调用。
所以:
- 想少写样板代码、避免漏掉
set_exception,优先用std::packaged_task - 需要精细控制异常构造时机(比如包装成自定义错误码再抛)、或 promise 生命周期需独立于任务逻辑时,才用裸
std::promise - 两者底层共享同一套
exception_ptr机制,性能无差别
最易被忽略的是 promise 的生命周期管理——它不像 std::packaged_task 那样自带执行语义,一旦忘记 set_xxx 或提前析构,就只剩 broken_promise 这个模糊错误,排查起来比异常本身还费时间。


















