How C++ coroutines work

Created 23rd June, 2017 05:21 (UTC), last edited 30th July, 2017 03:59 (UTC)

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:

  1. The state for which prime number we're at—the number member.
  2. The return of the prime number value—operator *
  3. Finding the next prime number—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.

Pages

  1. My first coroutine
  2. A more realistic coroutine
  3. Yielding Generators
  4. Generating Iterators

Categories: