Deep Engineering #59: Lieven De Cock on C++ Coroutines as a Build-It-Yourself Kit
The promise type, the coroutine handle, and why the boilerplate belongs in a library rather than your codebase
C++ Memory Management Deep Dive
Two live days with Patrice Roy, ISO C++ committee member and bestselling author, on exception-safe containers, allocator-aware programming, PMR, deferred memory reclamation, std::launder, std::bit_cast, and trivial relocatability.
🗓️ 22 to 23 August
Use code DEEPENG for 40% off.
✍️ From the editor’s desk,
Welcome to the 59th issue of Deep Engineering!
The WG21 August mailing closes tomorrow, so the papers that will shape C++29 are being finalized this week. C++26 was declared technically complete back in March, which puts the committee a full standard ahead of where most production code actually runs.
That distance is worth attending to. Writing up the meeting where C++26 was finished, Herb Sutter grouped coroutines with parallel STL, concepts and modules as features that “weren’t as massively impactful for all C++ developers as C++11’s features were.” Even with std::execution now in the standard, he warned, teams should expect to write their own adapter libraries before it connects to the async code they already have.
That is a fair description of what C++ has been doing for a decade. The language ships a primitive, the library layer follows a standard or two later, and in between teams either wait or build the missing piece themselves. We covered the same pattern in April with Sándor Dargó on C++26 adoption traps and the compiler gap. Coroutines are the clearest case, because C++20 shipped three keywords and C++23 shipped one library type to use them with.
Lieven De Cock, a C++ consultant and trainer at CppDriven with more than thirty years in the language, built three coroutines from scratch in his Deep Engineering Live session to show what that gap costs a team. His answer is not that coroutines are bad, but that the promise type, the handle and the iterator you write by hand are code your next reviewer will struggle to read, and that the real fix is std::generator, Boost.Asio, and whatever ships next.
Let’s get started.
Featured Newsletter: The Polymathic Engineer
Written by software engineer and craftsman Fernando Franco, with deep dives on algorithms and data structures, distributed systems and system design, software craftsmanship, and machine learning.
→ Subscribe to The Polymathic Engineer
🧠 Expert Insight
Building a C++ coroutine by hand, and why you probably should not
An excerpt from Lieven’s Deep Engineering deep dive.
I have something like thirty plus years of experience in C++ development, in different areas and different roles. I have been following the evolution of C++ closely, and at some point I changed the goal of my career to helping others tag along with that evolution. That is why I am now an independent consultant and coach, teaching not just the bare language and library but also the tooling and ecosystem around it, so teams can write more efficient and cleaner code. That is my mission for the rest of my career.
C++20 had the big four. Modules, ranges, concepts, and coroutines. There was a lot of fuss, and everybody had high expectations.
If we imagine coroutines as a nice book cabinet where you can store your books, that is what we were expecting. The reality is that in C++20 we got the build-it-yourself kit. That is one of the things a lot of people do not understand. Coroutines in C++20 are a language fundamental feature which allows you to build things, and that is also what we are going to do here. We are going to build up several coroutines, and we will see that we create a lot of boilerplate we would rather avoid.
How do we avoid that boilerplate in future? We use libraries that have support for coroutines. Boost.Asio, for example. We will look at a little example of that at the end.
So buckle up. We have some construction work to do.
Coroutines are not a multithreading feature
First, some myths. If you mention coroutines, people start anxiously jumping up and down saying yes, multithreading, asynchronous programming, that is what this is all about.
That is not true. Asynchronous work is one of the areas where coroutines shine, as we will see at the end. But it is just like an integer type, which also has nothing to do with multithreading, and which we still use in a multithreaded environment. Nearly all the examples here will be single threaded. Multithreading and asynchronous programming by themselves could take up another one or two sessions.
Before we go further, it is worth separating concurrency from parallelism, because the distinction is what makes coroutines interesting.
Picture a cook who is either chopping the carrot or stirring the pot. Chopping a bit, stirring a bit. That is concurrency. Both the chopping and the stirring make forward progress, but neither is happening at the same time. If we switch quickly enough between them, an observer might think both are progressing simultaneously, while they are not.
If, however, somebody is chopping and stirring at the very same time, a single person cannot do that. It would require a second cook, which is to say a second CPU, a second core. Then we have parallelism, where both are genuinely making progress at the same instant.
Every generation of this problem has been solved by making the switch cheaper
Let me go back in time, and this might tell my age.
In the mid eighties I got my first computer, a nice machine with big floppy disks, and I could run one program at a time. I inserted the floppy and started my word processing. It was not Microsoft Word back then, the king of the hill was WordPerfect. I would be editing text and get bored, and I would want to play a game. So I had to stop WordPerfect, insert another floppy disk, run Out Run, and go racing. Then when I had wasted enough time I stopped the game and started the word processor again. Nobody would call that concurrency. The swapping was far too slow to give any impression that both were progressing.
Then Windows came along, and by the mid nineties Windows 95. Now I was playing Pac-Man in one window, writing text in another, and the clock in the system tray was ticking the seconds away. It felt like everything was happening at once. It was the operating system switching the CPU between three processes, and PCs then were single core, so parallelism was not even possible. That was real concurrency, and switching between processes is something that in computer land takes a huge amount of time compared to running a single C++ statement. A completely different order of magnitude.
We had another problem in those days. If I filled in an input field, pressed calculate, and the calculation took a minute or two, then switched to my game and came back, I got a frozen GUI. The program had code to draw the interface, but the program was single threaded and the thread was doing the calculation.
That is what threads solved. Now the scheduler was not just handing the CPU to process one and then process two. It was handing the CPU to thread five of process one, then taking it away preemptively and giving it to thread one of process ten. Within my program I had a calculation thread and a GUI thread, and the GUI could refresh while the calculation continued. Switching between threads was much faster than switching between processes. But compared to a regular C++ statement, it is still extremely slow.
That is where coroutines come in. A coroutine runs a bit, then suspends, and something else can happen, maybe another coroutine, maybe the caller. That switch is of a completely different magnitude from a thread context switch. Way, way smaller. Way more efficient.
Coroutines are a collaboration. If a coroutine decides never to pause, it is not willingly giving up the CPU for anyone else, and you are back in the world where the scheduler eventually says you took enough time and takes the CPU away. But if it collaborates nicely, we get very quick switching between different pieces of the program.
A coroutine is a function that can be paused and resumed, which turns out to mean it is an object
What is a coroutine? It is a function that can be paused, suspended, and resumed. That is an absolutely correct definition. But what does it actually mean?
A regular function starts, does a job, and ends. It returns. With a coroutine you are saying that we start, and midway I want to pause, and later I want to continue where I left off. These are challenges we need to solve. The C++ coroutines ecosystem solves them, but it needs our help. That help is the part where we put the IKEA book cabinet together ourselves.
Here is the flow. Main is executing statements, and at some point it calls a coroutine. The coroutine starts, and at some point says it is going to suspend. We go back to the statement after the call, main continues, and then main resumes the coroutine. We pick up exactly where we left off, run more statements, suspend again, go back to main, and at some point the coroutine ends and hands control back completely.
Unless we put in effort for it to be otherwise, this is all on the same thread. By default the coroutine runs on the thread of the caller. It does not need to.
There is another way of looking at this. When we suspend, we do not have to return to our caller. We can go somewhere else entirely. That is the flow used in asynchronous environments, and we will come back to it.
So when is a function a coroutine? The moment one of three keywords appears in the function body. co_return, co_yield, or co_await. From that point the compiler knows this is a coroutine and does its magic, with the assistance of the programmer.
A quick note for later. co_yield you could read as here is a value to my caller, and then co_await. So co_yield is really here is the value, now I pause.
There are two sides to the story. There is the compiler-facing side, where the compiler recognizes the coroutine and needs information from us. And there is the user-facing side, where somebody uses that coroutine. We will fold these two angles together and meet somewhere in the middle.
A radio station, and why a regular function does not cut it
Let us build a first silly but educational example. We are going to create a radio station. We tell it what type of music we like and how many songs we want, and a radio station emerges that plays songs.
We ask the radio station to prepare a song. The DJ looks for the record, puts it on the turntable, and when it is done the radio station gives the song to us. Then it suspends. We listen to it, and afterwards we tell the coroutine to resume and put the next record on.
If we model this as a regular function, we pass in the style and the number of songs, we loop, and we play. The annoying thing is that the loop just continues. We get all the songs at once. If you are a DJ mixing, you might like that. If you want to listen, it is not a good user experience. So a regular function does not cut it.
What does it mean to model this as a coroutine? We create it, and that results in something. Different names exist in the literature. The coroutine interface, the coroutine API, the coroutine remote control, whatever you want to call it. After we create it, we get something we can interact with. Resume, for example. In our case that is please play the next song. Are there still songs to play, because if all the requested songs have been played it makes no sense to ask for another. Or maybe I am midway through song two and I realize I have to catch my train, and I want to stop.
We are professional programmers. Whatever we allocate, we want to deallocate. If we register, we unregister. If we create, we destruct.
That sounds like an object. You create it, it provides methods to interact with, and if you want to stop, the destructor takes care of it. So our function that can pause and resume is no longer just a simple function. It is an object. That is already a very interesting observation.
The state cannot live on the stack, so the burden comes back
This function has state. We pass in the type of music and the number of songs, and it needs those for the whole body. It gives us songs in a loop, so it needs to know which iteration it is on. When the coroutine is suspended, all of this needs to remain stored somewhere. It cannot be discarded, because resuming needs it.
Let us take a step back and look at what happens when we call a regular function. The caller puts the return address on the stack, so we know the next statement to execute when we come back. We put room on the stack for the return value. We put the arguments on the stack. We call the function, and its local variables go on the stack too. The stack keeps growing.
Then the function returns. It does not matter whether that is an early return, the closing brace, or an exception. Unwinding happens, everything is cleaned up, and the stack is back exactly as it was before the call.
Now imagine we want to resume after that has happened. We are in serious trouble, because we have learned there is state that needs to be preserved. So this state can no longer live on the stack, because it would all be gone.
If we cannot store it on the stack, what else do we have? The heap. Obvious choice. That is why C++ coroutines are called stackless. They store their state on the heap.
And we all know what using the heap means. We know we need to deallocate. That is the programmer’s responsibility. Not too early, and do not forget it.
A coroutine stores its state on the heap, and then it says, I have this coroutine frame here, dear developer, I want to give you a handle to it, and now it is up to you to free it at the correct time.
We were so used to smart pointers that we do not worry about this anymore. Well, ladies and gentlemen, this burden is back on our shoulders.
Continue reading for the promise type, the coroutine handle, the awaiter, and why Boost.Asio makes most of that boilerplate unnecessary.
You can also watch the full session here.
In case you missed
Building a C++ Coroutine by Hand, and Why You Probably Should Not
Three coroutines built from scratch, the boilerplate each one needs, and the library support that makes most of it unnecessary
🛠️ Tool of the Week
stdexec — the C++26 std::execution reference implementation
stdexec is the header-only reference implementation of P2300, the std::execution proposal accepted into C++26, with no external dependencies.
Lets you co_await senders directly inside coroutines and pass awaitables to sender algorithms, so senders are awaitable and awaitables are senders
Ships structured concurrency primitives including async_scope, task, finally, when_any and repeat_n, alongside composable algorithms like then, let_value, when_all and split
Supports pluggable schedulers covering a static thread pool, a Linux io_uring context, NVIDIA GPU contexts, and your own
Composes at compile time with no runtime allocations or reference counting
📎 Tech Briefs
GCC 16.2 released - GCC 16.2 fixes regressions while C++20 remains the default mode for builds omitting standard flags.
CNCF graduates Cloud Native Buildpacks - Buildpacks reached graduated status after security review, giving platform teams a vendor-neutral source-to-OCI path.
Microsoft ships its August Patch Tuesday - Microsoft patched exploited CVE-2026-68820, making AFD.sys updates a priority in enterprise patch queues.
KubeCon North America adds an AI Inference and Agentic track - GPU scheduling, model serving, and inference observability get a dedicated production-AI track at KubeCon North America.
The WG21 August mailing closes tomorrow - C++ papers submitted by Friday enter the next committee batch after the post-Brno mailing cycle.
That’s all for today. Thank you for reading this issue of Deep Engineering.
We’ll be back next week with more expert-led content.
Keep building,
Saqib Jan
Editor-in-Chief, Deep Engineering
If your company wants to reach senior developers, software engineers, and technical decision-makers, speak to us about partnering with Deep Engineering.









