C++协程销毁顺序疑问:伪代码与实际行为不符?
某文章给出了编译器转换协程函数的伪代码:
ReturnType someCoroutine(Parameters parameter) { auto* frame = new coroutineFrame(std::forward<Parameters>(parameters)); auto returnObject = frame->promise.get_return_object(); co_await frame->promise.initial_suspend(); try { <body-statements> } catch (...) { frame->promise.unhandled_exception(); } co_await frame->promise.final_suspend(); delete frame; return returnObject; }
这段伪代码暗示:如果协程的final_suspend返回一个continuation(即需要挂起并切换到其他协程),刚结束的协程内存会在continuation运行完成后才释放。但实际运行以下最小化示例时,内存释放发生在continuation之前:
#include <concepts> #include <coroutine> #include <exception> #include <iostream> #include <vector> template<class T = void> class Future; template<> class Future<void> { public: class Awaiter; class promise_type { private: std::exception_ptr _exception; std::coroutine_handle<> _continuation; friend class Awaiter; public: Future<void> get_return_object() { return {HandleT::from_promise(*this)}; } static Future<void> get_return_object_on_allocation_failure() { abort(); } // Futures are lazy, they don't do anything until they are // awaited or a task is spawned that takes one. std::suspend_always initial_suspend() { return {}; } // Always cleanup right away. Maybe not the right choice? std::suspend_never final_suspend() noexcept { std::cerr << "final_suspend this=" << this << std::endl; return {}; } // Need this definition to avoid UB void return_void() { std::cerr << "returning void this=" << this << std::endl; } // futures are not generators. Also note there is no // `yield_void`, standard is inconsistent. std::suspend_always yield_value(...) = delete; void unhandled_exception() { _exception = std::current_exception(); } void* operator new(std::size_t size) noexcept { auto p = malloc(size); std::cerr << "new p=" << p << std::endl; return p; } void operator delete(void* p, std::size_t size) noexcept { std::cerr << "delete p=" << p << std::endl; } }; struct Awaiter { private: Future<void>& future; public: explicit Awaiter(Future<void>& future) : future(future) {} // this will check if the future is already done, e.g. if it // was `co_await`ed previously bool await_ready() const noexcept { return future._done; } // this coroutine is the one doing the `co_await` std::coroutine_handle<> await_suspend(std::coroutine_handle<> handle) noexcept { // remember we want to resume back into this caller later future._handle.promise()._continuation = handle; // For now let the coroutine associated with this future // run, via "symmetric transfer", which takes the caller // resume call off the stack and replaces it with this // coroutine. return future._handle; } // this runs in the caller's task when they wake back up from having resume called? void await_resume() const noexcept { future._done = true; } }; auto operator co_await() { return Awaiter(*this); } void resume() { _handle.resume(); } protected: using HandleT = std::coroutine_handle<promise_type>; Future(HandleT&& p) : _handle(std::move(p)) {} Future(const Future&) = delete; Future& operator=(const Future&) = delete; Future(Future&& other) = delete; Future& operator=(Future&& other) = delete; friend class Awaiter; bool _done = false; HandleT _handle; std::exception_ptr _exception = nullptr; }; __attribute__((noinline)) Future<void> b() { co_return; } __attribute__((noinline)) Future<void> a() { co_await b(); } int main() { auto fut = a(); std::cerr << "first resume" << std::endl; fut.resume(); std::cerr << "second resume" << std::endl; fut.resume(); std::cerr << "exiting main" << std::endl; }
运行输出如下(地址相差16字节属于同一协程:0x6424502582b0与0x6424502582c0对应协程a,0x642450258310与0x642450258320对应协程b),可见协程b的内存先于a释放:
g++-14 -std=c++23 -g3 -fcoroutines example.cpp && ./a.out new p=0x6424502582b0 first resume new p=0x642450258310 returning void this=0x642450258320 final_suspend this=0x642450258320 delete p=0x642450258310 second resume returning void this=0x6424502582c0 final_suspend this=0x6424502582c0 delete p=0x6424502582b0 exiting main
请问这是编译器优化、伪代码错误,还是我触发了未定义行为(UB)只是未崩溃?
解答
这既不是编译器优化也不是未定义行为,而是伪代码的表述没有完全体现C++协程标准中的细节逻辑。
你的示例中,协程b的final_suspend返回的是std::suspend_never,这意味着协程执行到final_suspend时不会挂起。根据C++标准,当final_suspend返回非挂起的awaiter时,协程帧会在final_suspend执行完成后立即销毁——这正是你看到的delete p=0x642450258310出现在b的final_suspend之后的原因。
而伪代码里的co_await frame->promise.final_suspend(); delete frame;是一种简化表述,它只覆盖了final_suspend返回挂起awaiter的场景:此时协程会挂起,delete frame不会立即执行,直到该协程的handle被销毁或者显式调用destroy()。但如果final_suspend返回的是suspend_never,编译器会直接在final_suspend执行后销毁帧,不需要等待任何continuation。
你的代码逻辑是合规的,没有触发未定义行为。伪代码只是没有区分final_suspend返回不同awaiter类型的情况,属于表述不完整,而非错误。
内容的提问来源于stack exchange,提问作者Joseph Garvin

