If you look at coroutines in other language, JavaScript or Python for example, you'll see that the language documents how the coroutines work. How you can use co_yield
and co_await
etc. (however they're spelled) and what the language does for you with them. This is all very useful, and lets you do a lot of cool things with them, but always within the confines of what the language runtime allows.
In C++ they're quite different. The compiler provides a scaffolding on which you can decide how things work. This means that C++ doesn't provide generators, but it provides you a way to write a ton of different generators that work in different ways depending on your needs.
This is what this series of articles is going to be about. Not how to use or write coroutines that work according to some pre-established pattern, but how the underlying machinery allows you to customise the way that the coroutines work.
Let's start by looking at just one of the things we can use coroutines for, generating values.
We can write a simple function that prints prime numbers like this (I'm going to presuppose we have a suitable is_prime
function):
void primes1(std::size_t max) { for ( unsigned int number = 2; number < max; ++number ) { if ( is_prime(number) ) { std::cout << number << " is prime" << std::endl; } } } primes1(30);
But what if we don't want to print them, we want to do something else with them? The obvious thing, at least for modern C++ code, is to use a callback:
template<typename L> void primes2(std::size_t max, L found) { for ( unsigned int number = 2; number < max; ++number ) { if ( is_prime(number) ) found(number); } } primes2(30, [](auto n) { std::cout << n << " is prime" << std::endl; });
Callbacks are great, but they have a problem in that we now need to write inside out code when we want to get prime numbers.
The older solution to this problem, writing an iterator, solves this for the consumer of the prime numbers (they don't have inversions of control and inside out code), but it makes the implementation of the iterator much more complex (because now that has to be inside out):
class prime_iterator { unsigned int number = 2; public: auto operator * () const { return number; } prime_iterator &operator ++ () { for ( ++number; not is_prime(number); ++number ); return *this; } }; for ( prime_iterator p; *p < 30; ++p ) { std::cout << *p << " is prime" << std::endl; }
This particular iterator implementation is quite incomplete, but it does show that the logic we want is spread out and inside out. From the client code's perspective though consumption of the values is very simple and idiomatic, although this code is too simple to be usable in a ranged for loop.
What we've had to do is to split the generation of the prime numbers up into three parts:
number
member.operator *
operator ++
For prime numbers this isn't too bad, but for more complex calculations this transformation can be very hard to visualise and implement, and is the source of many bugs.
What we really want is to have something as easy to write as the first example, but is as easy to use the last one—maybe even easier if we can get it.
It turns out that this is exactly what co_yield
is made for, and it allows us to implement and use this:
generator primes3(std::size_t max) { for ( unsigned int number = 2; number < max; ++number ) { if ( is_prime(number) ) { co_yield number; } } } for ( auto v : primes3(30) ) std::cout << v << " is prime" << std::cout;
All we have to do is to learn how to implement classes like generator
so that things work.