DEV Community

Dave Cridland
Dave Cridland

Posted on • Edited on

Coroutines in C++

What's a Coroutine?

In a typical function call - known as a subroutine - you enter at the top, and exit once the subroutine is done. There's multiple paths through, potentially, but the caller's view of it is a single step.

A coroutine allows that path to be suspended at various points, and control handed back to the caller several times, or onto another coroutine. A caller might well see multiple steps during the invocation, or might deliberately choose to pause and wait for a result.

Types of coroutine

There's various things that are proper subsets of what you can do with coroutines. Python's generators were arguably the first popular use of the concept (though the original concept of coroutines is back in 1956, and generators themselves appeared in languages back in the 70's).

Asynchronous code, where an async function can "await" another's result, has similarly become popular due to both Python and Javascript - here, the caller suspends until the called coroutine is able to return a value (even if it, too, suspends).

Operational Polymorphism

Object-oriented programming, or OOP, covers a very wide set of concepts, but the most misunderstood is polymorphism. Polymorphism simply means that two or more distinct object types can handle the same operations (or, in stricter OOP terminology, process the same messages).

Most people thinking about OOP will be thinking about Inclusional Polymorphism - or inheritance of classes. But most modern languages allow other forms of polymorphism. C++'s templates (and to a degree, Java's Generics) are an example of Parametric Polymorphism, but much idiomatic C++ relies on a simpler form, where the strict type is secondary to the desired operations.

This is known as Operational Polymorphism, Row Polymorphism, or simply Duck Typing.

An example is the idiomatic foreach loop from C++03:


for (iterator i = container.begin(); i != container.end(); ++i) {
  // Do something with (*i).
}

By C++14, this had become such a concrete idiom that it gained a syntactic shorthand:


for (auto & a : container) {
  // Do something with a.
}

But a foreach loop doesn't need a particular container supertype - it can operate on anything that exposes both a begin() and an end() method. If it looks like a container type, and walks like a container type, then it is a container type.

C++ Coroutines

The coroutine TS in C++ is much the same. A Coroutine is, essentially, a function containing at least one coroutine keyword, with a special return type which provides the right methods. The return type needn't inherit from anything at all.

The primary interface is that the return type of the function has to have a nested type of promise_type. That, in turn, has to support an interface depending on what keywords are used.

Optionally - but sensible to do - is to have a constructor for the return type itself that accepts a Coroutine handle (specifically, std::experimental::coroutine_handle<promise_type>). That's a library-provided handle that includes some utility methods like resume(), promise(), and done().

Different methods are called on the promise at each "suspension point" - the beginning, the end, anytime a coroutine keyword is used, and so on.

The keywords are:

co_return - This suspends execution and calls promise_type::return_value(T t), where T is some type compatible with whatever you're trying to return. At its simplest, a coroutine using co_return instead of simply return isn't very useful - but you can suspend the coroutine in promise_type::initial_suspend() before it starts doing any work, giving you lazy - and indeed conditional - execution.

co_yield - This is similar to co_return, except you can have many of them, and they call promise_type::yield_value(T t), and after resuming the coroutine can continue executing. With this, you can build generators - and if your return type suports begin() and end(), you can iterate through with normal idiomatic C++.

co_await - This is an operator which - if called on something that either has the right interface or an operator overload for co_await - allows suspension of both the caller and the called coroutine. The interface is a little more complex.

Conclusion

Coroutines have become a fairly established part of modern programming, and are well worth exploring. C++'s approach is different to other major languages because it allows for a very flexible system - you can build all manner of higher-level primitives from it.

While this might seem daunting, bear in mind that this is the same approach as C++ used for containers, and this has lead to a powerful suite of algorithms and container types which the programmer can add to without constraint.

More information

The best tutorial on how the primitives work is Kirit Sælensminde's excellent one: How C++ coroutines work - but the nature of them is that you'll likely just grab a library that gives you a higher level anyway, like Conduit, or CppCoro.

You'll need a compiler that supports them - this is still a TS, so we're in experimental territory - but modern CLang will let you play (you'll need libc++ and libc++abi too, mind).

Have fun!

Top comments (0)