C++20 introduced coroutines. Quasar Chunawala, our guest editor for this edition, gives an overview.
| Introducing Quasar Chunawala – our Guest Editor | |
|
You’ve likely heard about this new C++20 feature, coroutines. I think that this is a really important subject and there are several cool use-cases for coroutines. A coroutine in the simplest terms is just a function that you can pause in the middle. At a later point the caller will decide to resume the execution of the function right where you left off. Unlike a function therefore, coroutines are always stateful - you at least need to remember where you left off in the function body.
Coroutines can simplify our code! Coroutines are a great tool, when it comes to implementing parsers.
The coroutine return type
The initial call to the coroutine function will produce a return object of a certain ReturnType and hand it back to the caller. The interface of this type is what is going to determine what the coroutine is capable of. Since coroutines are super-flexible, we can do a whole lot with this return object. If you have some coroutine, and you want to understand what it’s doing, the first thing you should look at is the ReturnType, and what it’s interface is. The important thing here is, we design this ReturnType. If you are writing a coroutine, you can decide what goes into this interface.
How to turn a function into a coroutine?
The compiler looks for one of the three keywords in the implementation: co_yield, co_await and co_return.
| Keyword | Action | State |
|---|---|---|
co_yield |
Output | Suspended |
co_return |
Output | Ended |
co_await |
Input | Suspended |
In the preceding table, we see that after co_yield and co_await, the coroutine suspends itself and after co_return, it is terminated (co_return is the equivalent of the return statement in the C++ function).
Use-cases for coroutines
Asynchronous computation. Suppose we are tasked with designing a simple echo server. We listen for incoming data from a client socket and we simply send it back to the client. At some point in our code for the echo server, we will have a piece of logic like that in Listing 1: we certainly don’t want to write a server like this. Say one of the clients requests communication and we are in a session. They say, they are ready to send the data, so we are blocking on the read, but maybe they send us this data in 2 minutes, or 5 minutes or even more. And other clients keep waiting.
void session(Socket sock){
char buffer[1024];
int len = sock.read({buffer});
sock.write({buffer,len});
log(buffer);
}
|
| Listing 1 |
One solution is to use an asynchronous framework and rewrite our code as in Listing 2.
void session(Socket sock){
struct State{ Socket sock; char buffer[1024];
};
// Heap allocate the state
auto state = std::make_shared<State>(sock,
buffer);
auto on_read_finished_callback = [state](
error_code ec,
size_t len
)
{
auto done = [state](error_code ec, size_t len)
{
if(!ec)
log();
}
if(!ec)
{
// Perform an asynchronous write
state->socket.async_write(
state->buffer,
done
);
}
}
// Perform an asynchronous read
state->socket.async_read( state->buffer,
on_read_finished_callback );
}
|
| Listing 2 |
So, the session makes two associations:
On finishing read ↦ on_finished_read_callback
On finishing write ↦ done
And implicitly there is a third association even though we cannot see it here – this entire function session is most likely a callback, in response to an event like On client connection established.
Accepting a new client connection ↦ session
So, the server will be many different associations of events to callbacks at different levels.
Pay attention to the state. We said that, we wanted to allocate it on the heap and manage it through a shared_ptr. We pass this shared_ptr<State> by value to every single callback. This way, I make sure that the last one who touches this session turns off the lights and deallocates state.
While this is a toy-example, in real production code, there can be a long sequence of steps and calling lambdas inside lambdas can obfuscate the meaning of the code.
A coroutine implementation of the same echoing session would look like this:
Task<void> session(Socket sock){
char buffer[1024];
int len = co_await sock.async_read({buffer});
co_await sock.async_write({buffer,len});
log(buffer);
}
This looks very similar to the sequential code, except that we use this co_await keyword. You have clear indication of the points where the coroutine will be suspended. Also, note that previously the function session returned void. Now, we are returning something: a Task<void>. This will be a handle to the coroutine and it’s how the outside world will be communicating with the coroutine.
Suspended computation. A second use-case is that coroutines support lazy evaluation. Lazy evaluation doesn’t do any work unless it’s absolutely necessary. This can also potentially make your code more efficient. Lazy evaluation also supports programming with infinite lists.
The lay of the land
The diagram in Figure 1 shows the relationships between the components of coroutines [Weis2022].
![]() |
| Figure 1 |
ReturnType – The initial call to the coroutine returns an object of the type ReturnType. This interface determines what the coroutine is actually capable of.
Promise – If we think of the coroutine as the producer of data and the caller as the consumer, on the producer side, the coroutine will store the result in a promise object. On the consumer side, the caller can retrieve the result using the return object of type ReturnType. So, the promise is the interface through which the caller interacts with the coroutine.
coroutine_handle – The coroutine_handle is like a raw pointer to the coroutine frame. The coroutine frame consists of the values of local variables, the promise and any internal state.
Awaitable – A coroutine may send – aka yield – a value to the caller or may (co)await an asynchronous operation to complete. In both cases, the coroutine will suspend itself, save its state in the coroutine frame and return control to the caller. An awaitable is therefore the thing the coroutine awaits on.
Reference
[Weis2022] Andreas Weis, ‘Deciphering Coroutines’, CppCon 2022 , available at https://www.youtube.com/watch?v=JXZswq3m41I.
has a bachelor’s degree in Computer Science. He is a software programmer turned quant engineer, enjoys building things ground-up and is deeply passionate about programming in C++, Rust and concurrency and performance-related topics. He is a long-distance hiker and his favorite trekking routes are the Goechala route in Sikkim, India and the Tour-Du-Mont Blanc (TMB) circuit in the French Alps.










