Coroutines are a major addition to the C++20 language standard, and so should be interesting to anyone wishing to develop their use of Modern C++. Coroutines are different to threads (which have been standardized since C++11) and are considerably more lightweight in their Standard Library implementation, requiring just the <coroutine>
header. An imperfect analogy could be that threads are similar to pre-emptive multitasking, while coroutines tend to mirror co-operative multitasking. Coroutines are not a replacement for threads, and the two can usefully co-exist; a coroutine can be paused on one thread and re-started on another, for example. An attractive feature of coroutines is that they are less susceptible to data races, deadlock, and some of the other pitfalls when using threads.
So the down side? While the change to the language itself is only the addition of three new keywords, co_await
, co_yield
and co_return
, a large amount of boilerplate (even by C++ standards) is needed to allow them to do anything useful. There is a three-way interaction between the language keywords used in your coroutine code, the Standard Library implementation, and the boilerplate they require as the “glue” between them. This article intends to cover co_yield
, with a focus on getting it to act like Python’s yield
keyword when constructing generators.
A function is automatically a coroutine if it contains one or more of co_await
, co_yield
or co_return
. Such functions must not exit using plain return
(an implicit co_return
is inserted immediately before the closing brace) and the return type cannot be auto
-deduced. (There are a number of other restrictions, see the link at the bottom of this article.)
A simple coroutine program, which counts 0, 1, 2 and exits, is shown below:
#include "Generator.hpp"
#include <iostream>
Generator<int> generator() {
for (int i = 0; i != 3; ++i) {
co_yield i;
}
}
int main() {
auto gen = generator();
while (gen) {
std::cout << "i = " << gen() << '\n';
}
}
The function generator()
is similar to a factory function, and returns a Generator<int>
object, which is assigned to the variable gen
in main()
. (I’m not sure why there is no std::generator
class template defined in the <coroutine>
header, the required Generator.hpp
is available from cppreference.com or the GitHub link below.) No output is produced until the while
-loop in main()
invokes gen()
, which pauses at the co_yield i
and immediately returns to main()
. Its state remains intact (the value of variable i
) ready for the next invocation; when the for
-loop is “done”, querying gen
returns false
. The output from this program is:
i = 0
i = 1
i = 2
It is possible to write a coroutine generator that never ends, without causing an infinite loop in the code which uses it. An example of this is shown below:
Generator<int> generator() {
int i = 0;
while (i != 6) {
co_yield i++;
}
while (true) {
co_yield i;
}
}
int main() {
auto gen = generator();
for (int j = 0; j != 10; ++j) {
std::cout << "i = " << gen() << '\n';
}
}
The number of calls to gen()
is now fixed at 10, using a for
-loop in main()
. Due to the second while
-loop in generator()
, it will always yield a result. The output from this program is:
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 6
i = 6
i = 6
A slightly more complex example, being a generator for the factorial function n!, is shown below. Note that the generator()
function now takes a parameter, which is the number of factorials, starting at 0!, to generate. The early co_return
handles the empty case, while the first co_yield
handles 0! when gen()
is invoked from main()
. The second co_yield
in the for
-loop handles 1! to (n-1)! and when this loop finishes an implicit co_return
is encountered, meaning that testing gen
returns false
so the for
-loop in main()
ends.
Generator<int> generator(unsigned n) {
if (n == 0) {
co_return;
}
int f = 1;
co_yield f;
for (int i = 1; i != n; ++i) {
co_yield f *= i;
}
}
int main() {
auto gen = generator(10);
for (int i = 0; gen; ++i) {
std::cout << i << "! = " << gen() << '\n';
}
}
That concludes our look at writing generators using C++ coroutines. Hopefully the syntax of co_yield
and co_return
has been made obvious, and the reader is encouraged to investigate the commented source file Generator.hpp
to gain further understanding (this is copied from cppreference.com). In the next article of this mini-series we’ll take a look at how co_await
can be used from within a coroutine.
Resources:
- Coroutines explained at cppreference.com
- Source code for these example programs on GitHub