Vittorio Romeo likes C++20 coroutine syntax and argues it is a terrible fit for game development. The objection is not speed. It is that a suspended coroutine’s state lives in a compiler-managed frame you cannot inspect, so it cannot be serialized.
That matters because a paused coroutine in a game is game data. “We are on phase 3 with 0.3 seconds of wait remaining” has to travel with the boss’s health and position through saves, quick-loads, replay, and network sync. The standard model treats that state as invisible scaffolding.
His specific objections:
- Heap allocation is optional, not guaranteed. Heap Allocation eLision Optimization is permitted, applied inconsistently across compilers, and can silently disappear when a coroutine body changes or a handle escapes. Debug and release builds can differ, with no attribute to force it.
std::coroutine_handleis opaque. There is no portable way to ask which locals exist or where execution is suspended, so serializing a paused coroutine is impossible without compiler hooks.- The plumbing is heavy. A working coroutine type needs a promise type, awaitables,
await_transform, and suspend functions before any game logic gets written. - Wrong shape anyway. Coroutines fit transient work — an async request, a finite generator. Game logic is not transient; it belongs to an object and must save, load, and sync with it.
sfex::Coroutine is the alternative: roughly 200 lines built on the old switch plus state-integer trick from the Protothreads lineage. The entire suspension state is one int plus whatever members you declare on the struct. It is allocation-free, deterministic (no hidden compiler lowering), trivially serializable, and composable — a coroutine can await another coroutine in straight-line code.
Two implementation details carry the design:
- He uses
__COUNTER__rather than__LINE__, because line numbers shift under cosmetic edits: a blank line, a new comment, or aclang-formatrun would make every saved integer resume at a random instruction. Adding or removing a yield still invalidates older saves — the same hazard any state machine enum has — so saves need a revision tag or high-level identity stored beside the integer. - Anything that must survive a yield has to be a struct member, because suspending literally means returning: the stack unwinds and RAII guards declared before a yield fire at the yield. The compiler will not catch most of these mistakes.
The state-machine comparison is fair rather than loaded. For a flat “do this, wait, do that, wait” sequence, Romeo says plainly that the minimal step-counter FSM is shorter and saves the same number of bytes. The coroutine wins once a script nests control flow or composes child behaviours: an FSM has to flatten every child’s states into its own enum and track which one is running, while the coroutine version is a call and a bubbled-up yield.
The reusable idea is the framing. If a feature’s state cannot be enumerated, saved, and copied cheaply, it is a liability in a deterministic game loop — and 200 lines of macro is a reasonable price for state you own.