You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++协程销毁顺序疑问:伪代码与实际行为不符?

关于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 05:54:53