Article by Ayman Alheraki on April 4 2026 02:02 PM
Coroutines stand as one of the most significant additions to the C++ language in the C++20 standard, promising a revolution in how we write concurrent and asynchronous software. Yet, unlike languages such as Python, Go, or Rust, where coroutines are widely adopted and simple to use, the average C++ programmer still approaches them with caution.
This technical article goes beyond merely explaining "how to write a coroutine." It dives deep into the technical and structural reasons behind their slow adoption, analyzes how the C++23 standard attempted to bridge the gap, and explores what is expected in C++26 to finally deliver on the magical promise of this technology.
The C++ standardization committee made a crucial decision when designing coroutines for C++20: to deliver only the Core Language Infrastructure and leave the building of the Standard Library support for the future. This is the root cause of their current difficulty of use.
In other languages, the language and its standard library provide everything: keywords (async/await) and the "Executor" (Runtime/Loop).
In C++20, we received:
Keywords: co_await (suspend execution), co_yield (produce a value and suspend), co_return (terminate execution and return a value).
Compiler Model: How the compiler transforms a standard function into a "state machine" that can be frozen and resumed.
Low-Level API: A set of very complex types and interfaces that the programmer must implement so the language understands how to handle the coroutine.
To use a coroutine in C++20, you must build a "mini-framework" yourself. You are required to manually define:
promise_type: An object that controls the coroutine's lifecycle, memory allocation, exception handling, and return value.
Awaitable object: An object that determines whether the coroutine will suspend (co_await) or continue, and how it will be resumed.
Memory Management: One must carefully manage the memory allocated by the compiler to store the coroutine's state (Coroutine frame allocation). This is often allocated on the heap, requiring optimizations to avoid performance penalties.
The Result: Writing a simple "Hello World" with an asynchronous call requires dozens of lines of complex boilerplate code that demands a deep understanding of compiler internals. This is not a model ripe for general adoption.
The standards committee realized that the lack of standard support was hindering coroutine adoption. In the C++23 standard, a significant, though focused, improvement was added:
std::generator (from the <generator> library)This addition made one specific use case of coroutines extremely easy: Lazy Value Generation.
The Benefit: A programmer no longer needs to implement a promise_type or an Awaitable just to create a value generator.
Example: Now, you can write a function that generates an infinite Fibonacci sequence, or reads a file line-by-line without loading it entirely into memory, using co_yield with absolute simplicity.
But the gap remained: C++23 did not solve the problem of Asynchronous I/O or Tasks (like async/await in other languages for networking or disk I/O operations).
Even with the language infrastructure in place, there is a central missing piece for real-world asynchronous coroutine operation: the "Executor."
A coroutine is just a function that can be suspended. But who resumes it? And when?
You need an event loop or a thread pool to manage these suspended tasks.
In C++20/23, there is no standard Executor. This means:
If you want to use coroutines today for Asynchronous I/O, you are forced to use third-party libraries like Boost.Asio or cppcoro.
These libraries built their own Executors, which leads to an "incompatibility" problem; you cannot easily mix coroutines from the Asio library with coroutines from another library because each has its own system for scheduling and resumption.
This heavy dependency on massive libraries like Boost has led many existing projects, or programmers seeking a standard, easy solution, to avoid coroutines entirely.
There is great hope for the C++26 standard. The primary goal is to introduce Execution (std::execution), formerly known as proposal P2300, which is expected to be the comprehensive solution to all these problems.
Instead of every library creating its own Executor, the C++26 standard will provide a unified interface for how tasks are scheduled and executed.
Key Components:
Schedulers: Represent where the code will run (e.g., thread_pool, run_loop, gpu).
Senders: Represent the work that will be done (can be a coroutine, or just a function).
Receivers: Represent the object that receives the result.
With std::execution:
It will be very easy to transform any coroutine into a "Sender" that can be scheduled on any "Scheduler."
The standard will provide standard types (like std::task) that use the new Execution model, allowing the average programmer to write async/await without worrying about low-level promise types.
Most Importantly: Coroutines from different libraries will become compatible with each other because they use the same standard language for Execution.
| C++ Standard | Coroutine State | Ease of Use for Average Programmer | Recommendation |
|---|---|---|---|
| C++20 | Language infrastructure only (low-level). No standard library. | Very difficult and frustrating (requires third-party libraries). | Do not attempt to write the infrastructure yourself. Use it only if you are building a framework. |
| C++23 | Added std::generator (for lazy generation). | Easy and exceptional for Generators only. | Use it immediately for lazy generators. |
| C++26 (Expected) | Added std::execution (unified executors) and coroutine support. | Will become much easier, and true adoption as a standard thread alternative is expected. | This is the ultimate goal. Look forward to adopting it upon release. |
If you want to use coroutines today for Asynchronous I/O, the best choice is to use Boost.Asio. It is a solid, proven library that provides robust coroutine support. But if you are looking for a standard, easy, and universally compatible solution, wait for the C++26 standard, which will deliver the magical solution we have all been waiting for.