C++20协程:co_await后循环捕获异常访问e.what()触发段错误的解决方法
e.what()触发段错误问题 我在co_await一个抛出异常的协程后,尝试在catch块中访问异常详情(e.what())时遇到了段错误。异常能被成功捕获,但访问其任何方法都会导致崩溃。
重要提示:该问题仅在使用for循环时出现。如果移除循环,仅保留单个try { co_await task2; } catch (...)块,程序运行正常。
最小可复现代码示例
#include <Windows.h> #include <coroutine> #include <exception> #include <iostream> #include <stdexcept> struct Task2 { struct promise_type { std::exception_ptr* exception_; auto get_return_object() { return Task2{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_never initial_suspend() { return {}; } std::suspend_always // 故意设置:尝试保持帧存活 final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() { *exception_ = std::current_exception(); } }; Task2(std::coroutine_handle<promise_type> h) : handle_(h) { h.promise().exception_ = &exception_; } ~Task2() { std::cout << "Task2 destroyed" << std::endl; if (handle_) { handle_.destroy(); } } std::coroutine_handle<promise_type> handle_; std::exception_ptr exception_; bool await_ready() { return true; } void await_suspend(std::coroutine_handle<>) {} void await_resume() { if (exception_) { std::rethrow_exception(exception_); } } }; struct Task1 { struct promise_type { auto get_return_object() { return Task1{std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_never initial_suspend() { return {}; } std::suspend_always // 故意设置:尝试保持帧存活 final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; Task1(std::coroutine_handle<promise_type> h) : handle_(h) {} ~Task1() { std::cout << "Task1 destroyed" << std::endl; if (handle_) { handle_.destroy(); } } std::coroutine_handle<promise_type> handle_; }; Task2 FThrow() { throw std::runtime_error("TEST_EXCEPTION"); co_return; } Task1 Fmain() { for (int i = 0; i < 10; ++i) { // 问题仅在循环中出现 std::cout << "Main promise iteration " << i << std::endl; auto task2 = FThrow(); try { co_await task2; } catch (std::exception const& e) { std::cout << "TEST " << e.what() << std::endl; // 此处触发段错误 } } co_return; } int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) { auto task = Fmain(); return 0; }
预期行为
程序应输出:
Main promise iteration 0 TEST TEST_EXCEPTION Task2 destroyed Main promise iteration 1 ...
(Task1/Task2的析构函数应在循环/程序结束时调用)
实际行为
程序输出:
Main promise iteration 0 TEST
随后在调用e.what()时崩溃并触发段错误。
关键问题:崩溃前析构函数未被调用——输出中既没有"Task2 destroyed"也没有"Task1 destroyed",这意味着段错误发生时Task2对象(及其存储的协程句柄)仍然存在。
启用页堆的WinDbg显示崩溃发生在0xCDCDCDCD(未初始化内存模式),表明异常对象已被释放。
调查
- 异常对象通过
std::current_exception()捕获并存储在std::exception_ptr中 - 异常在
await_resume()中被成功重新抛出并捕获 - 在MSVC中,被
std::exception_ptr捕获的异常对象分配在协程帧(coroutine frame)中 - 我故意在
final_suspend()中使用suspend_always,并在析构函数中手动调用destroy(),试图保持帧存活直到catch处理完成 - 析构函数在崩溃前未打印输出,说明段错误发生时Task2对象仍存在,但异常对象已被释放
- 该问题仅在循环中出现——单个try/catch可正常运行
- 调试观察:在第41行(
std::rethrow_exception(exception_);)断点时,调试器中的exception_变量看起来有效,即使发生段错误后其地址在调试视图中仍显示正确,这表明std::exception_ptr本身是正常的。
假设
我怀疑FThrow()的协程帧(异常对象所在位置)在await_resume()重新抛出异常后、catch处理程序访问e.what()之前被销毁。
流程似乎是:
FThrow()抛出异常→被unhandled_exception()捕获→存储为std::exception_ptrawait_resume()调用std::rethrow_exception(exception_)→异常传播- 协程帧在此处被销毁——即使持有句柄的Task2对象仍然存在
- 异常在catch块中被捕获,但实际异常对象已不存在
- 调用
e.what()会解引用已释放的内存→段错误 - 之后Task2析构函数才会运行(但我们先崩溃了)
核心谜团:Task2析构函数在崩溃前未运行(无输出),说明Task2对象仍存在且应持有带有final_suspend()中suspend_always的协程句柄,但帧却已被销毁。
可能的解释:
await_ready()返回true意味着协程从未真正挂起,使得final_suspend()中的suspend_always失去意义- 帧在异常抛出后立即被销毁,无论挂起设置如何
std::exception_ptr实际上并未保持异常对象存活,仅指向它
环境
- Clang-CL(带MSVC兼容层的LLVM/Clang编译器)[测试过多个版本]
- MSVC标准库版本19.42(Visual Studio 2022)
- C++20(
/std:c++20) - Windows 11
- 启用页堆的Debug构建
构建命令行
Clang-CL
编译命令:
C:\PROGRA~1\LLVM\bin\clang-cl.exe /nologo -TP -DNOMINMAX /DWIN32 /D_WINDOWS /EHsc /Zi /Ob0 /Od /RTC1 -clang:-std=c++20 -MDd /W4 -clang:-DDEBUG -clang:-Wall -clang:-Wextra -clang:-Wpedantic -clang:-Wcast-align -clang:-Waddress-of-packed-member -clang:-Werror -clang:-ftemplate-backtrace-limit=0 -clang: -clang:-O0 -clang:-g /showIncludes /Fobuild\promise_test.cpp.obj /Fdbuild\ -c -- promise_test.cpp
链接命令:
C:\PROGRA~1\LLVM\bin\lld-link.exe /nologo build\promise_test.cpp.obj /out:build\promise_test.exe /implib:build\promise_test.lib /pdb:build\promise_test.pdb /version:0.0 /machine:x64 /debug /INCREMENTAL /subsystem:windows kernel32.lib
MSVC
编译命令:
cl.exe /nologo /TP /DNOMINMAX /DWIN32 /D_WINDOWS /EHsc /Zi /Od /RTC1 /std:c++20 /MDd /W4 /DDEBUG /showIncludes /Fobuild\promise_test.cpp.obj /Fdbuild\ -c promise_test.cpp
链接命令:
link.exe /nologo build\promise_test.cpp.obj /out:build\promise_test.exe /implib:build\promise_test.lib /pdb:build\promise_test.pdb /version:0.0 /machine:x64 /debug /INCREMENTAL /subsystem:windows kernel32.lib
问题
如何在co_await后安全捕获并访问协程抛出的异常,避免出现野指针? 是否有办法确保协程帧(以及异常对象)在catch处理程序完成前保持有效,或者这是C++20协程的基本限制,需要采用不同的错误处理模式?
具体问题:
- 我关于帧销毁时机的假设是否正确?
await_ready()返回true是否会绕过挂起机制并导致帧立即销毁?- 是否有正确的方法保持帧存活直到异常处理完成?
- 这可能是编译器优化错误导致的吗?
更新
将异常按引用传入协程帧会重现相同的崩溃,因此问题不限于帧分配的异常对象。
int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) { std::runtime_error e{"TEST_EXCEPTION"}; auto task = Fmain(e); return 0; } ... Task2 FThrow(std::runtime_error const& e) { throw e; co_return; }
内容的提问来源于stack exchange,提问作者Alex

