C++20: cppcoro coroutines

The cppcoro library from Lewis Baker gives us something that C++20 doesn’t give us – a C++ coroutine abstraction library based on Coroutines TS.

Maybe my last two posts”C++20: infinite data stream with coroutines” and “C++20: thread synchronization with coroutines” were quite difficult to understand. But the following posts about coroutines should be easier to digest. I generously provided them with examples of existing coroutines cppcoro.
To simplify my argument a bit, I want to start with a few words about coroutines and the coroutine framework.
cppcoro
The cppcoro library by Lewis Baker is based on Coroutine TS. TS stands for technical specifications (technical specs) and is a preview of the coroutine framework we got in C++20. Lewis is porting the cppcoro library from the Coroutines TS framework to the Coroutines framework we got in C++20.
I believe there is one reason why porting a library is very important: in C++20, we didn’t get the coroutines themselves, but only the coroutine framework. This nuance means that if you want to use coroutines in C++20, then you are on your own. You need to create your own coroutines based on the C++20 coroutine framework. Most likely, we will get some specific coroutines only in C++23. To be honest, I think this is extremely important, because the implementation of coroutines is quite complex and therefore error prone. This gap is exactly what cppcoro fills. It provides abstractions for coroutines, awaitable types, functions, cancellation, schedulers, networking, metafunctions, and defines several concepts.
Using cppcoro
cppcoro is currently based on the Coroutines TS framework and can be used on Windows (Visual Studio 2017) or Linux (Clang 5.0/6.0 and libc++). For all the examples in our experiments, I will use the configuration you can see on the command line below:

-std=c++17
: C++17 support-fcoroutines-ts
: Coroutines TS C++ support-Iinclude
: cppcoro headers-stdlib=libc++
: LLVM standard library implementationlibcppcoro.a
: cppcoro library
As I mentioned earlier: when cppcoro is updated to C++20 in the future, you will be able to use it with every compiler that supports C++20. And now, with the help of it, we can get some idea about the specific implementations of coroutines that we can see in C++23.
I found cppcoro to be easy to learn and I want to show you some examples of how to use it. To demonstrate the various features of cppcoro, I’m using snippets from existing code and tests. Let’s start with coroutine types.
Coroutine types
cppcoro has different kinds of tasks (task) and generators (generator).
task
What is a task? Here is a definition taken straight from the documentation:
task is an asynchronous computation that is performed lazily, since the execution of the coroutine does not begin until the task becomes awaited.
A task is a coroutine. In the following program, the function main
expects a function first
, first
expects second
a second
expecting a third.
// cppcoroTask.cpp
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <cppcoro/sync_wait.hpp>
#include <cppcoro/task.hpp>
using std::chrono::high_resolution_clock;
using std::chrono::time_point;
using std::chrono::duration;
using namespace std::chrono_literals; // 1s
auto getTimeSince(const time_point<high_resolution_clock>& start) {
auto end = high_resolution_clock::now();
duration<double> elapsed = end - start;
return elapsed.count();
}
cppcoro::task<> third(const time_point<high_resolution_clock>& start) {
std::this_thread::sleep_for(1s);
std::cout << "Third waited " << getTimeSince(start) << " seconds." << std::endl;
co_return; // (4)
}
cppcoro::task<> second(const time_point<high_resolution_clock>& start) {
auto thi = third(start); // (2)
std::this_thread::sleep_for(1s);
co_await thi; // (3)
std::cout << "Second waited " << getTimeSince(start) << " seconds." << std::endl;
}
cppcoro::task<> first(const time_point<high_resolution_clock>& start) {
auto sec = second(start); // (2)
std::this_thread::sleep_for(1s);
co_await sec; // (3)
std::cout << "First waited " << getTimeSince(start) << " seconds." << std::endl;
}
int main() {
std::cout << std::endl;
auto start = high_resolution_clock::now();
cppcoro::sync_wait(first(start)); // (1)
std::cout << "Main waited " << getTimeSince(start) << " seconds." << std::endl;
std::cout << std::endl;
}
As you can see, this program doesn’t really do much other than serve as a demonstration of a coroutine slave.
First, the function main
cannot be a coroutine. cppcoro::sync_wait
(line (1)) often serves as a top-level start task and waits for the task to complete (not just in this example). Coroutine first
like all other coroutines, receives as an argument start
(work start time) and displays the time of its execution. What happens in this coroutine? She runs a coroutine second
(line (2)) which immediately pauses, sleeps for a second, and resumes the coroutine with a handle sec
in line (3). Coroutine second
does the same with third
but third
a different behavior. third
– a coroutine that does not return anything and does not expect another coroutine. When third
finishes execution, all other coroutines are executed along the chain. Ultimately, the work of each of the coroutines takes three seconds.

Let’s change the program a little. What happens if coroutines go to sleep after being called co_await
?
// cppcoroTask2.cpp
#include <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <cppcoro/sync_wait.hpp>
#include <cppcoro/task.hpp>
using std::chrono::high_resolution_clock;
using std::chrono::time_point;
using std::chrono::duration;
using namespace std::chrono_literals;
auto getTimeSince(const time_point<::high_resolution_clock>& start) {
auto end = high_resolution_clock::now();
duration<double> elapsed = end - start;
return elapsed.count();
}
cppcoro::task<> third(const time_point<high_resolution_clock>& start) {
std::cout << "Third waited " << getTimeSince(start) << " seconds." << std::endl;
std::this_thread::sleep_for(1s);
co_return;
}
cppcoro::task<> second(const time_point<high_resolution_clock>& start) {
auto thi = third(start);
co_await thi;
std::cout << "Second waited " << getTimeSince(start) << " seconds." << std::endl;
std::this_thread::sleep_for(1s);
}
cppcoro::task<> first(const time_point<high_resolution_clock>& start) {
auto sec = second(start);
co_await sec;
std::cout << "First waited " << getTimeSince(start) << " seconds." << std::endl;
std::this_thread::sleep_for(1s);
}
int main() {
std::cout << std::endl;
auto start = ::high_resolution_clock::now();
cppcoro::sync_wait(first(start));
std::cout << "Main waited " << getTimeSince(start) << " seconds." << std::endl;
std::cout << std::endl;
}
Perhaps you have already guessed. Function main
waits for three seconds, but each subsequent call to the coroutine reduces the wait by one second.

In future posts, I’m going to look at tasks in combination with threads and signals.
generator
Here is the definition from cppcoro:
generator
represents a coroutine type that creates a sequence of type valuesT
where values are created lazily and synchronously.
No extra introductions to the program cppcoroGenerator.cpp
which demonstrates the operation of two generators:
// cppcoroGenerator.cpp
#include <iostream>
#include <cppcoro/generator.hpp>
cppcoro::generator<char> hello() {
co_yield 'h';
co_yield 'e';
co_yield 'l';
co_yield 'l';
co_yield 'o';
}
cppcoro::generator<const long long> fibonacci() {
long long a = 0;
long long b = 1;
while (true) {
co_yield b; // (2)
auto tmp = a;
a = b;
b += tmp;
}
}
int main() {
std::cout << std::endl;
for (auto c: hello()) std::cout << c;
std::cout << "\n\n";
for (auto i: fibonacci()) { // (1)
if (i > 1'000'000) break;
std::cout << i << " ";
}
std::cout << "\n\n";
}
First coroutine hello
returns the next character on request; coroutine fibonacci
the next number in the Fibonacci series. fibonacci
creates an endless stream of data. What happens in line (1)? Cycle for
with a range starts coroutine execution. The first iteration runs coroutines, returns a value co_yield b
and pauses. Subsequent calls to this loop resume the coroutine fibonacci
and return the next number from the Fibonacci series.

And finally, I would like to give an intuitive understanding of the difference between co_await
(for the task) and co_yield
(for generator): co_await
waiting inside, co_yield
waiting outside. For example, coroutine first
waiting for a coroutine to be called second
(cppcoroTask.cpp
), and the coroutine fibonacci
(cppcoroGenerator.cpp
) is triggered by the outer loop for
with range.
What’s next?
In my next post about cppcoro, we will delve into the tasks. I’ll cover their use with threads, signals, or thread pools.
The translation of the article was prepared on the eve of the start of the course “C++ Developer. Professional”. Pass entrance testingif you are curious to know your level of knowledge for admission.