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

C++20协程是否支持计算goto?求高效可移植替代方案

问题描述

我用GNU计算goto扩展结合C++20协程实现了一个JSON解析器,原本预期二者搭配能实现高效的执行逻辑,但截至2023年,编译器对该组合的支持存在严重兼容性问题:

  • GCC trunk版本可正常编译,但GCC 12.2(包括我使用的xtensa GCC 12.2和riscv GCC 12.2)会触发悬垂指针警告
  • Clang trunk直接禁止在协程中使用计算goto扩展

复现代码

#include <cstdio>
#include <coroutine>
#include <string_view>
#include <tuple>
#include <stdexcept>
#include <array>

struct Promise;

struct Parser : public std::coroutine_handle<Promise> {
    using promise_type = ::Promise;
    ~Parser() { destroy(); }
};

struct Awaitable {
    auto await_ready() -> bool { printf("await_ready!\n"); return false; }
    auto await_suspend(std::coroutine_handle<Promise> handle) {}
    auto await_resume() -> void {};
};

struct Promise {
    std::string_view pstrv_;
    auto get_return_object() -> Parser { return {Parser::from_promise(*this)}; };
    auto initial_suspend() noexcept -> std::suspend_never { return {}; }
    auto final_suspend() noexcept -> std::suspend_always { return {}; }
    auto await_transform() -> Awaitable { return{}; }
    auto return_void() { printf("return void!\n"); }
    auto unhandled_exception() {}
};

template <typename T>
auto coro() -> Parser {
    void* continuation_hook = &&done;
    continuation_hook = &&add_object_item;
    goto *continuation_hook;

    printf("Not printed!\n");

add_object_item:
    printf("Worked!\n");

done:
    printf("done!\n");
    co_return;
}

int main() {
    Parser myparser = coro<int>();
}

编译器错误信息

GCC 12.2警告

<source>:40:23: 警告:将局部变量“add_object_item”的地址存储到“*frame_ptr.coro<int>()::_Z4coroIiE6Parserv.Frame::continuation_hook_1_2”中 [-Wdangling-pointer=]
   40 |     continuation_hook = &&add_object_item;
      |     ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~
<source>:46:1: 备注:“add_object_item”在此处声明
   46 | add_object_item:
      | ^~~~~~~~~~~~~~~
<source>:46:1: 备注:“frame_ptr”在此处声明

Clang trunk错误

<source>:38:31: 错误:协程中不允许使用GNU标签地址扩展。
    void* continuation_hook = &&done;
                              ^
<source>:40:25: 错误:协程中不允许使用GNU标签地址扩展。
    continuation_hook = &&add_object_item;
                        ^

我需要一种同样高效、可移植性更强的替代方案,以适配目标微控制器环境。


解决方案

1. 用标准C++状态机替代计算goto

计算goto的核心是手动控制执行流跳转,我们可以用枚举类型定义解析状态,在协程中通过switch实现状态切换,完全符合C标准,所有支持C20协程的编译器都能兼容,且编译器会对状态机做优化,效率接近计算goto。

修改后的示例代码:

#include <cstdio>
#include <coroutine>
#include <string_view>

struct Promise;

struct Parser : public std::coroutine_handle<Promise> {
    using promise_type = ::Promise;
    ~Parser() { destroy(); }
};

struct Awaitable {
    auto await_ready() -> bool { printf("await_ready!\n"); return false; }
    auto await_suspend(std::coroutine_handle<Promise> handle) {}
    auto await_resume() -> void {};
};

struct Promise {
    std::string_view pstrv_;
    // 将状态存储在promise中,协程挂起后状态不会丢失
    enum class State {
        Initial,
        AddObjectItem,
        Done
    } state_ = State::Initial;

    auto get_return_object() -> Parser { return {Parser::from_promise(*this)}; };
    auto initial_suspend() noexcept -> std::suspend_never { return {}; }
    auto final_suspend() noexcept -> std::suspend_always { return {}; }
    auto await_transform() -> Awaitable { return{}; }
    auto return_void() { printf("return void!\n"); }
    auto unhandled_exception() {}
};

template <typename T>
auto coro() -> Parser {
    auto& promise = co_await std::suspend_always{}; // 获取promise引用
    auto& state = promise.state_;

    while (true) {
        switch (state) {
            case Promise::State::Initial:
                state = Promise::State::AddObjectItem;
                continue; // 模拟跳转到add_object_item
            case Promise::State::AddObjectItem:
                printf("Worked!\n");
                state = Promise::State::Done;
                continue;
            case Promise::State::Done:
                printf("done!\n");
                co_return;
        }
    }
}

int main() {
    Parser myparser = coro<int>();
}

优势:完全标准C++,无扩展依赖;状态存储在promise中,避免悬垂指针问题;编译器可优化状态机为跳转指令,效率接近计算goto。

2. 利用协程的自然挂起/恢复设计解析流程

JSON解析是典型的流式分步任务,协程的核心优势就是支持异步/分步执行,我们可以把每个解析步骤拆分为协程的挂起点,让协程自然管理执行流,无需手动跳转。

示例思路:

// 伪代码示例
Parser parse_json(std::string_view input) {
    // 解析JSON开头
    if (!consume(input, '{')) co_await suspend_to_continue();
    // 解析对象项
    while (true) {
        auto key = co_await parse_string(input);
        if (!key) break;
        auto value = co_await parse_value(input);
        // 处理键值对
        handle_key_value(key, value);
        if (consume(input, '}')) break;
    }
    co_return;
}

优势:符合C++协程设计范式,代码可读性更强;无需手动管理状态跳转,编译器自动处理执行流;完全标准,兼容性拉满。

3. 用状态机生成器生成高效代码(如Ragel)

如果追求极致性能且愿意引入工具链依赖,可以用Ragel这类状态机生成器,它能根据正则表达式或状态图生成高度优化的C/C代码,再与C20协程结合。Ragel生成的代码是标准C++,无GNU扩展依赖,且性能可媲美计算goto。

操作步骤:

  1. 用Ragel语法定义JSON解析的状态转换规则
  2. 生成C++状态机代码
  3. 将生成的代码嵌入到协程中,通过协程挂起/恢复控制解析进度

优势:生成的代码经过极致优化,性能远超手写状态机;可移植性强,生成的代码兼容所有标准C++编译器;适合性能敏感的微控制器场景。


内容的提问来源于stack exchange,提问作者glades

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 20:43:16