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。
操作步骤:
- 用Ragel语法定义JSON解析的状态转换规则
- 生成C++状态机代码
- 将生成的代码嵌入到协程中,通过协程挂起/恢复控制解析进度
优势:生成的代码经过极致优化,性能远超手写状态机;可移植性强,生成的代码兼容所有标准C++编译器;适合性能敏感的微控制器场景。
内容的提问来源于stack exchange,提问作者glades
相关产品推荐
相关产品推荐

