Chapter 24: Coroutines

Consider the following assignment: design a program that offers a function next returning the next fibonacci number at subsequent calls. (Fibonacci numbers start with 0 and 1. The next fibonacci number is the sum of the last two fibonacci numbers. The sequence, therefore, starts with 0, 1, 1, 2, 3, 5, etc. In this and the following examples fibonacci sequences are frequently used for illustration, and not because they are inherently related to coroutines.)

Here is an example of how such a program could be designed: it defines a class Fibo and in main Fibo::next is called to compute the next fibonacci number (for brevity the program uses a single source file):

    #include <iostream>
    #include <string>

    using namespace std;

    class Fibo
    {
        size_t d_return = 0;
        size_t d_next = 1;

        public:
            size_t next();
    };

    size_t Fibo::next()
    {
        size_t ret = d_return;      // the next fibonacci number

        d_return = d_next;          // at the next call: return d_next;
        d_next += ret;              // prepare d_next as the sum of the
                                    // original d_return and d_next
        return ret;
    }

    int main(int argc, char **argv)
    {
        Fibo fibo;                  // create a Fibo object

        size_t sum = 0;

                                    // use its 'next' member to obtain
        for (                       // the sequence of fibonacci numbers
            size_t begin = 0, end = argc == 1 ? 10 : stoul(argv[1]);
                begin != end;
                    ++begin
        )
            sum += fibo.next();

        cout << sum << '\n';
    }

Clearly the next member isn't all that complicated. But when it's called several actions are performed which themselves are unrelated to computing fibonacci numbers:

These steps, although they look like a lot, in practice don't take that much time, because most of them are performed by very fast register operations, and the computer' architecture is usually highly optimized for these steps.

Nonetheless, in situations where the functions themselves are short and simple (like Fibo::next) these steps, requiring stack- and register manipulations, might be considered unwelcome, raising the question whether they may be avoided.

C++ coroutines allow us to avoid executing the steps that are required when calling ordinary functions. The upcoming sections cover coroutines in depth, but here is, for starters, the coroutine's equivalent of Fibo::next:

     1: #include "main.ih"
     2: 
     3: Fibo fiboCoro()
     4: {
     5:     size_t returnFibo = 0;
     6:     size_t next = 1;
     7: 
     8:     while (true)
     9:     {
    10:         size_t ret = returnFibo;
    11: 
    12:         returnFibo = next;
    13:         next += ret;
    14: 
    15:         co_yield ret;
    16:     }
    17: }

Already now we can observe some characteristics of the fiboCoro coroutine:

While fiboCoro lays the foundation for obtaining a sequence of fibonacci numbers, resuming a coroutine doesn't mean calling a function the way the above member Fibo::next is called: there are no arguments; there is no preparation of local variables; and there is no stack handling. Instead there's merely a direct jump to the instruction just beyond the coroutine's suspension point. When the coroutine's code encounters the next suspension point (which occurs in fiboCoro at it's next cycle, when it again reaches its co_yield statement) then the program's execution simply jumps back to the instruction following the instruction that resumed the coroutine.

The main function using the coroutine looks very similar to the main function using the class Fibo:

     1: #include "main.ih"
     2: 
     3: int main(int argc, char **argv)
     4: {
     5:     Fibo fibo = fiboCoro();
     6: 
     7:     size_t sum = 0;
     8: 
     9:     for (                       // the sequence of fibonacci numbers
    10:         size_t begin = 0, end = argc == 1 ? 10 : stoul(argv[1]);
    11:             begin != end;
    12:                 ++begin
    13:     )
    14:         sum += fibo.next();
    15: 
    16:     cout << sum << '\n';
    17: }

At line 5 fiboCoro is called, returning an (as yet not covered) fibo thing. This fibo thing is called a coroutine handler, and when (in line 14) fibo.next() is called, then that call resumes fiboCoro, which is then again suspended at its co_yield statement, returning the next available value through the handler's next function.

The core feature of coroutines is thus that they may suspend their execution, while keeping their current state (values of variables, location of the next instruction to be executed, etc.). Normally, once suspended, the coroutine's caller resumes its work beyond the instruction that returned the coroutine's next value, so those two functions (the coroutine and its caller) closely cooperate to complete the implemented algorithm. Co-routines are therefore also known as cooperating routines, which should not be confused with concurrent (multi-threaded) routines.

How this whole process works, and what its characteristics are, is covered in the upcoming sections.

24.1: Defining a coroutine

When defining coroutines the <coroutine> header file must be included.

A function is a coroutine once it uses the keywords co_yield, co_await, or co_return. A coroutines cannot use the return keyword, cannot define variadic parameters, and its return type must be an existing type, which defines the coroutine's handler.

Although coroutines appear to return objects (as suggested by the Fibo return type of the Fibo fiboCoro() coroutine defined in the previous section), in fact they do not. Instead coroutines return so-called `handlers'. Such a handler is fibo, defined and used in the previous section's main function:

    int main(int argc, char **argv)
    {
        auto fibo = fiboCoro();
        ...
        sum += fibo.next();
        ...
    }

The class Fibo itself defines the characteristics allowing the compiler to generate code storing the coroutine's arguments, its local variables, the location of the next instruction to execute when the coroutine returns or is suspended, and its so-called promise_type object on the heap. This only happens once, so when the coroutine is activated (as in sum += fibo.next()) the steps which are normally taken when a function is called are avoided, and instead the coroutine is immediately executed, using its already available local variables and arguments. Coroutine's handler classes are sometimes called Future, and their nested state classes must be known as the handler's promise_type. The names future and promise_type are completely unrelated to the std::future (cf. section 20.8) and std::promise (cd. section 20.12) types which are used in the context of multi threading. In fact, coroutines themselves are unrelated to multi threading, but are known as cooperating routines. Because the coroutines' handler and state classes are unrelated to the future and promise classes used in the context of multi threading in this chapter the terms Handler and State are generally used.

It's one thing to define a coroutine, but when using a coroutine its handler-class (the Fibo class in the current example) must also be defined. In addition, such a handler-class must define a nested class whose name must be publicly available as the handler's promise_type. The name promise_type doesn't very well cover its purpose, and using a more descriptive class name might be preferred. In that case a simple using declaration in the handler class's public section can be used, as shown in the following basic design of the Fibo handler-class:

    #include <coroutine>

    class Fibo
    {
        class State     // keeps track of the coroutine's state
        {
            ...
        };

        std::coroutine_handle<State> d_handle;

        public:
            using promise_type = State;

            explicit Fibo(std::coroutine_handle<State> handle);
            ~Fibo();

            size_t next();
    };

The coroutine's handler class has the following characteristics:

The following members can be called via the Handler's d_handle data member:

The Handler's State class keeps track of the coroutine's state. Its basic elements are covered in the next section.

24.1.1: The coroutine's State class (promise_type)

The class Handler::State keeps track of the coroutine's state. It must publicly be known as the class Handler::promise_type, which can be realized using a public using-declaration associating a more appropriately named class with `promise_type'.

In the current example the class name State is used, having the following interface:

        class State
        {
            size_t d_value;

            public:
                Fibo get_return_object();

                std::suspend_always yield_value(size_t value);

                static std::suspend_always initial_suspend();
                static std::suspend_always final_suspend() noexcept;
                static void unhandled_exception();

                static void return_void();

                size_t value() const;
        };

This State class doesn't declare a constructor, so its default constructor is used. It's also possible to declare and define the default constructor. Alternatively, by declaring and defining a constructor that has the same parameters as its coroutine (or parameters that can be initialized by the coroutine's parameters) that constructor is called when the coroutine returns its handling object. E.g., if a coroutine's signature is

    Handler coro(int value, string const &str);
and the State class has a constructor
    Handler::State::State(int value, string const &str);
then that constructor is called. State's default constructor is called if such a constructor is not available. In addition to calling State's constructor a coroutine can also use an awaiter to pass arguments to the handler's State class. This method is covered in section 24.5.

The data member d_value and member function value() are specifically used by the class Fibo, and other coroutine state classes might declare other members. The remaining members are required, but the members returning std::suspend_always could also be declared as members returning std::suspend_never.

By returning the (empty) structs suspend_always the coroutine's actions are suspended until resumed. In practice suspend_always is used, and so the ..._suspend members can be declared static, using these basic implementations:

    inline std::suspend_always Fibo::State::initial_suspend() 
    { 
        return {}; 
    }
    inline std::suspend_always Fibo::State::final_suspend() noexcept
    { 
        return {}; 
    }
Likewise, the unhandled_exception member can be declared static when it simply retrows exceptions that may be thrown by the coroutine:
    inline void Fibo::State::unhandled_exception()
    {               // don't forget: #include <future>
        std::rethrow_exception(std::current_exception());
    }
The (required) member Fibo::State::get_return_object returns an object of the coroutine's handling class (so: Fibo). The recipe is:

Here is Fibo::State::get_return_object's implementation:

    inline Fibo Fibo::State::get_return_object()
    {
        return Fibo{ std::coroutine_handle<State>::from_promise(*this) };
    }
The member Fibo:State::yield_value can be overloaded for different argument types. In our Fibo::State there's only one yield_value member, storing its parameter value in the State::d_value data member. It also suspends the coroutine's execution as it returns std::suspend_always:
    inline std::suspend_always Fibo::State::yield_value(size_t value) 
    {
        d_value = value;
        return {};
    }
Now that the coroutine's handling class and its State subclass have been covered, let's have a closer look at what happens when the main function from the introductory section is executed. Here's main once again:
     1: int main(int argc, char **argv)
     2: {
     3:     auto fibo = fiboCoro();
     4: 
     5:     size_t sum = 0;
     6: 
     7:     for (                       // the sequence of fibonacci numbers
     8:         size_t begin = 0, end = argc == 1 ? 10 : stoul(argv[1]);
     9:             begin != end;
    10:                 ++begin
    11:     )
    12:         sum += fibo.next();
    13: 
    14:     cout << sum << '\n';
    15: }

When called with argument `2' the following happens:

24.1.1.1: What if `suspend_never' is used?

What if, instead of returning std::suspend_always State's members return std::suspend_never? In that case the coroutine, once it has started, is is never suspended. If the program computing fibonacci numbers is then called with argument 2, the following happens:

24.1.2: Simplifying the state class

Since the coroutine handler's state classes can often use the shown minimal implementations for its members, it might be attractive to define those members in a separate base-class, thus simplifying the state class's interface and implementation.

Looking at the Fibo::State class, its members initial_suspend, final_suspend and unhandled_exception are good candidates for such a base class. By defining the base class as a class template, receiving the coroutine handler's class name and the handler's state class name as its template type parameters then it can also provide the handler's get_return_object member.

Here is how this base class can be defined. It is used by the coroutine handler's state classes developed in this chapter:

    #include <cstddef>
    #include <future>
    #include <coroutine>
    
    template <typename Handler, typename State>
    struct PromiseBase
    {
        Handler get_return_object();
    
        static std::suspend_always initial_suspend();
        static std::suspend_always final_suspend() noexcept;
        static void unhandled_exception();
        static void return_void();
    };
    
    
    template <typename Handler, typename State>
    inline void PromiseBase<Handler, State>::return_void()
    {}
    
    template <typename Handler, typename State>
    inline std::suspend_always PromiseBase<Handler, State>::initial_suspend()
    {
        return {};
    }
    
    template <typename Handler, typename State>
    inline std::suspend_always PromiseBase<Handler, State>::final_suspend() noexcept
    {
        return {};
    }
    
    template <typename Handler, typename State>
    inline void PromiseBase<Handler, State>::unhandled_exception()
    {
        std::rethrow_exception(std::current_exception());
    }
    
    template <typename Handler, typename State>
    inline Handler PromiseBase<Handler, State>::get_return_object()
    {
        return Handler{ std::coroutine_handle<State>::from_promise(
                                                    static_cast<State &>(*this) )
                      };
    }

24.2: Embedding coroutines in classes

Coroutines do not have to be free functions (i.e., outside of classes). They can also very well be defined as class members, in which case they have full access to the members of their class.

In this section we develop a class Floats that can either write or read binary float values to or from files. An object of this class is used in main, calling its member run to either write or read a binary file (the full program is available in the distribution's coroutines/demo/readbinary directory):

    int main(int argc, char **argv)
    {
        Floats floats(argc, argv);
        floats.run();
    }

The program is called with two arguments: r for reading, or w for writing, and the name of the binary file as its second argument.

The member Floats:run uses pointers to members to call either read or write:

    class Reader;
    class Writer;
    
    class Floats
    {
        enum Action
        {
            READ,
            WRITE,
            ERROR,
        };
    
        Action d_action;
        std::string d_filename;                     // name of the binary file
    
        static void (Floats::*s_action[])() const;  // pointers to read and write
    
        public:
            Floats(int argc, char **argv);
            void run() const;
    
        private:
            void read() const;
            Reader coRead() const;
    
            void write() const;
            static Writer coWrite();
    
    };
    
    inline void Floats::run() const
    {
        (this->*s_action[d_action])();
    }

The member read reads the binary file, using the coroutine coRead. When coRead is called the usual actions are performed: implicitly the coroutine Reader's State member get_return_object is called to obtain the coroutine's handler, and the coroutine is suspended. Next the handler returned by get_return_object is made available as the read function's reader object:

    void Floats::read() const
    {
        Reader reader = coRead();           // coRead: the coroutine
                                            // reader: the coroutine's handler
        while (auto opt = reader.next())    // retrieve the next value
            cout << opt.value() << ' ';
    
        cout << '\n';
    }

Once the reader object is available the member read enters a while loop repeatedly calling reader.next(). At this point the following happens:

When resumed for the first time (so when reader.next() is called for the first time) the coRead coroutine opens the file, and then, in a while-statement, determines the next available value. If that succeeds the coroutine is again suspended, using co_yield to pass the just read value on to read, or (if no value could be obtained) the coroutine ends by calling co_return. Here is the Floats::coRead coroutine:

    Reader Floats::coRead() const
    {
        ifstream in{ d_filename };
    
        while (true)
        {
            float value;
            in.read(reinterpret_cast<char *>(&value), sizeof(float));
    
            if (not in)
                co_return;          // if not: end the coroutine
    
            co_yield value;
        }
    }

Likewise, the member write (re)writes the binary file, using the coroutine coWrite, following the same procedure as used by read to obtain the writer coroutine handler:

    void Floats::write() const
    {
        ofstream out{ d_filename };
    
        Writer writer = coWrite();          // coWrite: the coroutine,
                                            // writer: the coroutine's handler
        cout << "Enter values (one per prompt), enter 'q' to quit\n";
    
        while (true)
        {
            cout << "? ";
            auto opt = writer.next();       // retrieve the next value
            if (not opt)                    // stop if no more values
                break;
            out.write(&opt.value()[0], sizeof(float));
        }
    }

The member Floats::coWrite behaves like Floats::coRead, but writes instead of reads values to the binary file. Here is coWrite, defined as a normal (non-static) member, as it uses Floats::d_filename:

    Writer Floats::coWrite()
    {
        while (true)
        {
            float value;
            if (not (cin >> value))       // get the next value
                co_return;          // if not: end the coroutine
    
    
            Writer::ValueType ret;  // value to return
    
            ret = Writer::ValueType{ string{
                        reinterpret_cast<char const *>(&value),
                        reinterpret_cast<char const *>(&value + 1) }
                    };
    
            co_yield ret;
        }
    }

The Reader and Writer handler classes are covered next.

24.2.1: The `Reader' coroutine handler

The essence of the Reader class is that its State subclass receives a value from the coroutine (coRead) at the coroutine's co_yield statement. Reader::State receives the value that's passed to co_yield as argument of its yield_value member, which stores the received float value in its std::optional<float> d_value data member.

The Reader class itself must define a constructor receiving a handle to its State class , and should define a destructor. Its next member simply returns the value that's stored in its State class to next's caller. Here is Reader's complete header file:

    #include <iostream>
    #include <optional>
    
    #include "../../promisebase/promisebase.h"
    
    struct Reader
    {
        using ValueType = std::optional<float>;
    
        private:
            class State: public PromiseBase<Reader, State>
            {
                ValueType d_value;
    
                public:
                    std::suspend_always yield_value(float value);
                    void return_void();
                    ValueType const &value() const;
            };
    
            std::coroutine_handle<State> d_handle;
    
        public:
            using promise_type = State;
    
            explicit Reader(std::coroutine_handle<State> handle);
            ~Reader();
    
            ValueType const &next();
    };

Reader's and Reader::State's members have (except for Reader::next) very short implementations which can very well be defined inline:

    inline std::suspend_always Reader::State::yield_value(float value)
    {
        d_value = value;
        return {};
    }
    
    inline void Reader::State::return_void()
    {
        d_value = ValueType{};
    }
    
    inline Reader::ValueType const &Reader::State::value() const
    {
        return d_value;
    }
    
    inline Reader::Reader(std::coroutine_handle<State> handle)
    :
        d_handle(handle)
    {}
    
    inline Reader::~Reader()
    {
        if (d_handle)
            d_handle.destroy();
    }

Reader::next performs two tasks: it resumes the coroutine, and then, once the coroutine is again suspended (at its co_yield statement), it returns the value stored in the Reader::State object. It can access its State class object via d_handle.promise(), returning the value stored in that object:

    Reader::ValueType const &Reader::next()
    {
        d_handle.resume();
        return d_handle.promise().value();
    }

24.2.2: The `Writer' coroutine handler

The Writer class closely resembles the Reader class. It uses a different value type, as it must write float values to the output stream using their binary representations, but other than that there aren't that many difference with the Reader class. Here is its interface and the implementations of its yield_value member that differs from that of the Reader class:
    struct Writer
    {
        using ValueType = std::optional<std::string>;
    
        private:
            class State: public PromiseBase<Writer, State>
            {
                ValueType d_value;
    
                public:
                    std::suspend_always yield_value(ValueType &value);
                    void return_void();
                    ValueType const &value() const;
            };
    
            std::coroutine_handle<State> d_handle;
    
        public:
            using promise_type = State;
    
            explicit Writer(std::coroutine_handle<State> handle);
            ~Writer();
    
            ValueType const &next();
    };
    
    inline std::suspend_always Writer::State::yield_value(ValueType &value)
    {
        d_value = std::move(value);
        return {};
    }

24.3: `Awaitables', `Awaiters' and `co_await'

So far we've encountered co_yield and co_return. What about co_await? The verb to await is more formal than to wait, but the two verbs mean the same. The added level of formality of to await is illustrated by a second description offered by the Merrian Webster dictionary: to remain in abeyance until, and abeyance's meaning takes us home again: a state of temporary inactivity or suspension. So when co_await is used the coroutine enters a state of temporary inactivity, i.e., it is suspended. In that sense co_yield is no different, as it also suspends the coroutine, but different from co_yield co_await expects a so-called awaitable expression. I.e., an expression resulting in an Awaitable, or that is convertible to an Awaitable object (see also figure 31).

Figure 31: co_await

Figure 31 shows that the expression that's passed to co_await may be an Awaitable object, or if the coroutine handle's State class has a member await_transform accepting an argument of some expr's type, the value returned by await_transform is the Awaitable (cf. figure 32). These await_transform members may be overloaded, so in any concrete situation several Awaitable types could be used.

Figure 32: awaitable

The Awaiter type that's eventually used is either an object of co_await's expr's type, or it is the return type of the (implicitly called when defined) coroutine handler's State::await_transform(expr) member.

Thus, the Awaitable object is a middle-man, that's only used to obtain an Awaiter object. The Awaiter is the real work-horse in the context of co_await.

Awaitable classes may define a member Awaiter Awaitable::operator co_await(), which may also be provided as a free function (Awaiter operator co_await(Awaitable &&)). If such a co_await conversion operator is available then it is used to obtain the Awaiter object from the Awaitable object. If such a conversion operator is not available then the Awaitable object is the Awaiter object.

As an aside: the types Awaitable and Awaiter are used here as formal class names, and in actual programs the software engineer is free to use other (maybe more descriptive) names.

24.4: The class `Awaiter'

Once the Awaiter object is available its member bool await_ready() is called. If it returns true then the coroutine is not suspended, but continues beyond its co_await statement (in which case the Awaitable object and Awaiter::await_ready were apparently able to avoid suspending the coroutine).

If await_ready returns false Awaiter::await_suspend(handle) is called. Its handle argument is the handle (e.g., d_handle) of the current coroutine's handler object. Note that at this point the coroutine has already been suspended, and the coroutine's handle could even be transferred to another thread (in which case the current thread must of course not be allowed to resume the current coroutine). The member await_suspend may return void, bool, or some coroutine's handle (optionally its own handle). As illustrated in figure 33, when returning void or true the coroutine is suspended and the coroutine's caller continues its execution beyond the statement that activated the coroutine. If false is returned the coroutine is not suspended, and resumes beyond the co_await statement. If a coroutine's handle is returned (not a reference return type, but value return type) then the coroutine whose handle is returned is resumed (assuming that another coroutine's handle is returned than the current coroutine is suspended, and the other coroutine (which was suspended up to now) is resumed; in the next section this process is used to implement a finite state automaton using coroutines)

Figure 33: awaiter

If, following await_suspend, the current coroutine is again resumed, then just before that the Awaiter object calls Awaiter::await_resume(), and await_resume's return value is returned by the co_await expression (await_resume) frequently defines a void return type, as in

    static void Awaiter::await_resume() const
    {}

In the next section a finite state automaton is implemented using coroutines. Their handler classes are also Awaiter types, with await_ready returning false and await_resume doing nothing. Thus their definitions can be provided by a class Awaiter acting as base class of the coroutines' handler classes. Awaiter only needs a simple header file:

    struct Awaiter
    {
        static bool await_ready();
        static void await_resume();
    };
    
    inline bool Awaiter::await_ready()
    {
        return false;
    }
    
    inline void Awaiter::await_resume()
    {}

24.5: Accessing State from inside coroutines

As we've seen, when a coroutine starts it constructs and returns an object of its handler class. The handler class contains a subclass whose object keeps track of the coroutine's state. In this chapter that subclass is named State, and a using declaration is used to make it known as promise_type which is required by the standard facilities made available for coroutines.

When coroutines are suspended at co_yield statements, the yielded values are passed to State class's yield_value members whose parameter types match the types of the yielded values.

In this section we reverse our point of view, and discuss a method allowing the coroutine to reach facilities of the State class. We've already encountered one way to pass information from the coroutine to the State class: if the State class's constructor defines the same parameters as the coroutine itself then that constructor is used, receiving the coroutine's parameters as arguments.

But let's assume that the coroutine performs a continuous loop containing several, maybe conditional, co_yield statements, and we want to inform the State class what the current iteration cycle is. In that case a parameter is less suitable, as tracking the cycle number is in fact a job for one of the local variables of the coroutine, which would look something like this:

    Handler coroutine()
    {
        size_t cycleNr = 0;
        // make cycleNr available to tt(Handler's State) class
        while (true)
        {
            ++cycleNr;      // now also known to tt(Handler's State)
            ...             // the coroutine at work, using various co_yield
                            // statements
        }
    }
Awaiters can also be used in these kinds of situations, setting up communication lines between coroutines and the State classes of their Handler class objects. As an illustration, the original fibocoro coroutine 24 was slightly modified:
     1: Fibo fiboCoroutine()
     2: {
     3:     size_t returnFibo = 0;
     4:     size_t next = 1;
     5:     size_t cycle = 0;
     6: 
     7:     co_await Awaiter{ cycle };
     8:     cerr << "Loop starts\n";
     9: 
    10:     while (true)
    11:     {
    12:         ++cycle;
    13: 
    14:         size_t ret = returnFibo;
    15: 
    16:         returnFibo = next;
    17:         next += ret;
    18: 
    19:         co_yield ret;
    20:     }
    21: }

The Awaiter object, since there's no State::await_transform member, is an awaitable. Neither does Awaiter have a Type operator co_await(), so the anonymous Awaiter object is indeed an Awaiter.

Being the Awaiter, it defines three members: await_ready, merely returning false, as the coroutine's execution must be suspended at the co_await statement; await_suspend(handle), receiving a handle to the coroutine's Handler's State object; and await_resume, which doesn't have to do anything at all:

    class Awaiter
    {
        size_t const &d_cycle;
    
        public:
            Awaiter(size_t const &cycle);
    
            bool await_suspend(Fibo::Handle handle) const;
    
            static bool await_ready();
            static void await_resume();
    };
    
    inline Awaiter::Awaiter(size_t const &cycle)
    :
        d_cycle(cycle)
    {}
    
    inline bool Awaiter::await_ready()
    {
        return false;
    }
    
    inline void Awaiter::await_resume()
    {}

The member await_suspend uses the received handle to access the State object, passing cycle to State::setCycle:

    bool Awaiter::await_suspend(Fibo::Handle handle) const
    {
        handle.promise().setCycle(d_cycle);
        return false;
    }

In the next section (24.6) we use await_suspend to switch from one coroutine to another, but that's not required here. So the member returns false, and thus continues its execution once it has passed cycle to State::setCycle. This way coroutines can pass information to the Handler's State object, which could define a data member size_t const *d_cycle and a member setCycle, using d_cycle in, e.g., yield_value:

    inline void Fibo::State::setCycle(size_t const &cycle)
    {
        d_cycle = &cycle;
    }

    std::suspend_always Fibo::State::yield_value(size_t value)
    {
        std::cerr << "Got " << value << " at cycle " << *d_cycle << '\n';
        d_value = value;
        return {};
    }

24.6: Finite State Automatons via coroutines

Finite state automatons (FSAs) are usually implemented via state x input matrices. For example, when using Flexc++ to recognize letters, digits or other characters it defines three input categories, and 4 states (the first state being the INITIAL state determining the next state based on the character read from the scanner's input stream, the other three being the states that handle characters from their specific categories).

FSAs can also be implemented using coroutines. When using coroutines each coroutine handles a specific input category, and determines the category to use next, given the current input category. Figure 34 shows a simple FSA: at Start a digit takes us to Digit, a letter to Letter, at any other character we remain in Start, and at end of file (EOF) we end the FSA at state Done. Digit and Letter act analogously.

Figure 34: Finite State Automaton

This FSA uses four coroutines: coStart, coDigit, coLetter, and coDone, each returning their own handlers (like a Start handler returned by coStart, a Digit handler by coDigit, etc.). Here is coStart:

     1: Start coStart()
     2: {
     3:     char ch;
     4:     while (cin.get(ch))
     5:     {
     6:         if (isalpha(ch))
     7:         {
     8:             cout << "at `" << ch << "' from start to letter\n";
     9:             co_await g_letter;
    10:         }
    11:         else if (isdigit(ch))
    12:         {
    13:             cout << "at `" << ch << "' from start to digit\n";
    14:             co_await g_digit;
    15:         }
    16:         else
    17:             cout << "at char #" << static_cast<int>(ch) <<
    18:                     ": remain in start\n";
    19:     }
    20:     co_await g_done;
    21: }

The flow of this coroutine is probably self-explanatory, but note the co_await statements at lines 9, 14, and 20: at these lines the co_awaits realize the switch from the current coroutine to another. How that's realized is soon described.

The coroutines coDigit and coLetter perform similar actions, but coDone, called at EOF, simply returns, thus ending the coroutine-based processing of the input stream. Here's coDone, simply using co_return to end its lifetime:

    Done coDone()
    {
        cout << "at EOF: done\n";
        co_return;
    }

Now take a look at this short input file to be processed by the program:

    a
    1
    a1
    1a

when processing this input the program shows its state changes:

    at `a' from start to letter
    at char #10: from letter to start
    at `1' from start to digit
    at char #10: from digit to start
    at `a' from start to letter
    at `1' from letter to digit
    at char #10: from digit to start
    at `1' from start to digit
    at `a' from digit to letter
    at char #10: from letter to start
    at EOF: done

Since coroutines are normally suspended once activated, the Start handler privides a member go starting the FSA by resuming its coroutine:

    void  Start::go()
    {
        d_handle.resume();      // maybe protect using 'if (d_handle)'
    }

The main function merely activates the Start coroutine, but the coroutines might of course also be embedded in something like a class FSA, and main might offer an option to process a file argument instead of using redirection. Here's main:

    int main()
    {
        g_start.go();
    }

24.6.1: The `Start' handler class

As illustrated, there are various ways to obtain an Awaiter from a co_await expr statement. The shortest route goes like this:

So, the nested Start::State class only has to provide the standard members of the coroutine handler's State class. As those are all provided by the generic PromiseBase class (section 24.1.2) State needs no additional members:

                // nested under the Start handler class:
    struct State: public PromiseBase<Start, State> 
    {};
Similar considerations apply to the other three handler classes: their State classes are also derived from PromiseBase<Handler, State>. However, as the coDone coroutine also uses co_return, the Done::State state class must have its own a return_void member:
                // nested under the Done handler class:
    struct State: public PromiseBase<Done, State> 
    {
        void return_void() const;
    };
                // implementation in done.h:
    inline void Done::State::return_void() const
    {}
As our FSA allows transitions from Digit and Letter back to Start the Start handler class itself is an Awaiter (as are Digit, Letter, and Done). Section 24.4 described the requirements and basic definition of Awaiter classes.

From the point of view of FSAs the most interesting part is how to switch from one coroutine to another. As illustrated in figure 33 this requires a member await_suspend which receives the handle of the coroutine using the co_await statement, and returns some coroutine's handle. So:

Here is the interface of coStart's handler class as well as the definition of its await_suspend member. Since the coStart coroutine may be resumed by several other coroutines it is unknown which coroutine's handle was passed to Start::await_suspend, and so await_suspend is a member template, which simply returns Start's handle.

    class Start: public Awaiter
    {
        struct State: public PromiseBase<Start, State>
        {};
    
        std::coroutine_handle<State> d_handle;
    
        public:
            using promise_type = State;
            using Handle = std::coroutine_handle<State>;
    
            explicit Start(Handle handle);
            ~Start();
    
            void go();
    
                    // this and the  members in Awaiter are required for Awaiters
            template <typename HandleType>
            std::coroutine_handle<State> await_suspend(HandleType &handle);
    };
    
    template <typename HandleType>
    inline std::coroutine_handle<Start::State>
                                      Start::await_suspend(HandleType &handle)
    {
        return d_handle;
    }

As the member Start's wait_suspend returns Start's d_handle, the coroutine containing the co_await g_start statement is suspended, and the co_start coroutine is resumed (see also figure 33).

The implementations of the Start handler's constructor and destructor are straightforward: the constructor stores the coroutine's handle in its d_handle data member, the destructor uses the (language provided) member destroy to properly end the Start::State's coroutine handle. Here are their implementations:

    Start::Start(Handle handle)
    :
        d_handle(handle)
    {}

    Start::~Start()
    {
        if (d_handle)
            d_handle.destroy();
    }

24.6.2: Completing the Finite State Automaton

The Digit and Letter coroutines handler classes are implemented like Start. Like coStart, which continues its execution when a non-digit and non-letter character is received, coDigit continues for as long as digit characters are received, and coLetter continues for as long as letter characters are received.

As we've seen in section 24.6 the implementation of coDone is a bit different: it doesn't have to do anything, and the coDone coroutine simply ends at its co_return statement. Coroutine execution (as well as the FSA program) then ends following main's g_start.go() call.

The complete implementation of the coroutine-based FSA program is available in the Annotation's distribution under the directory yo/coroutines/demo/fsa.

24.7: Recursive coroutines

Like ordinary functions coroutines can recursively be called. An essential characteristic of coroutines is that when they're used they look no different than ordinary functions. It's merely in the implementation that coroutines differ from ordinary functions.

For starters, consider a very simple interactive program that produces a series of numbers until the user ends the program or enters q:

     1: int main()
     2: {
     3:     Recursive rec = recursiveCoro(true);
     4: 
     5:     while (true)
     6:     {
     7:         cout << rec.next() << "\n"
     8:                 "? ";
     9: 
    10:         string line;
    11:         if (not getline(cin, line) or line == "q")
    12:             break;
    13:     }
    14: }

At line 3 the recursiveCoro coroutine is called, returning its handler rec. In line 7 its member next is called, returning the next value produced by recursiveCoro. The function recursiveCoro could have been any function returning an object of a class that has a next member. For now ignoring recursion, recursiveCoro could look like this:

     1: namespace
     2: {
     3:     size_t s_value = 0;
     4: }
     5: 
     6: Recursive recursiveCoro(bool recurse)
     7: {
     8:     while (true)
     9:     {
    10:         for (size_t idx = 0; idx != 2; ++idx)
    11:             co_yield ++s_value;
    12: 
    13:         // here recursiveCoro will recursively be called 
    14: 
    15:         for (size_t idx = 0; idx != 2; ++idx)
    16:             co_yield ++s_value;
    17:     }
    18: }

The coroutine merely produces the sequence of non-negative integral numbers, starting at 0. Its two for-loops (lines 10 and 15) are there merely for illustrative purposes, and the recursive call will be placed between those for-loops. The variable s_value is defined outside the coroutine (instead of using static s_value = 0 inside), as recursively called coroutines must all access the same s_value variable. There's no magic here: just two for-statements in a continuously iterating while-statement.

The interface of the returned Recursive object isn't complex either:

     1: class Recursive
     2: {
     3:     class State: public PromiseBase<Recursive, State>
     4:     {
     5:         size_t d_value;
     6: 
     7:         public:
     8:             std::suspend_always yield_value(size_t value);
     9:             size_t value() const;
    10:     };
    11: 
    12:     private:
    13:         using Handle = std::coroutine_handle<State>;
    14:         Handle d_handle;
    15: 
    16:     public:
    17:         using promise_type = State;
    18: 
    19:         explicit Recursive(Handle handle);
    20:         ~Recursive();
    21: 
    22:         size_t next();
    23:         bool done() const;
    24: };

The required members of its State class are available in PromiseBase (cf. section 24.1.2) and do not have to be modified. As recursiveCoro co_yields values, the State::yield_value member stores those values in its d_value data member:

    std::suspend_always Recursive::State::yield_value(size_t value)
    {
        d_value = value;
        return {};
    }

Its member value is an accessor, returning d_value. When recursion is used the recursive calls end at some point. When recursiveCoro functions end State::return_void is called. It doesn't have to do anything, so PromiseBase's empty implementation perfectly does the job.

The Recursive handling class's own interface starts at line 12. Its d_handle data member (line 14) is initialized by its constructor (line 19), which is all the constructor has to do. The handler's destructor only has to call d_handle.destroy() to return the memory used by its State object.

The remaining members are next and done. These, too, are implemented straight-forwardly. The member done will shortly be used in the recursive implementation of recursiveCoro, and it just returns the value returned by d_handle.done().

When the member next is called the coroutine is in its suspended state (which is what happens when it's initially called (cf. line 3 in the above main function) and thereafter when it uses co_yield (lines 11 and 16 in the above implementation of recursiveCoro)). So it resumes the coroutine, and when the coroutine is again suspended, it returns the (next available) value stored in the handler's State object:

    size_t Recursive::next()
    {
        d_handle.resume();
        return d_handle.promise().value();
    }

24.7.1: Recursively calling recursiveCoro

Now we change the non-recursive recursiveCoro coroutine into a recursively called coroutine. To activate recursion recursiveCoro is modified by adding some extra statements below line 8:
     1: Recursive recursiveCoro(bool recurse)
     2: {
     3:     while (true)
     4:     {
     5:         for (size_t idx = 0; idx != 2; ++idx)
     6:             co_yield ++s_value;
     7: 
     8:         // here recursiveCoro will recursively be called
     9: 
    10:         if (not recurse)
    11:             break;
    12: 
    13:         Recursive rec = recursiveCoro(false);
    14: 
    15:         while (true)
    16:         {
    17:             size_t value = rec.next();
    18:             if (rec.done())
    19:                 break;
    20: 
    21:             co_yield value;
    22:         }
    23: 
    24:         for (size_t idx = 0; idx != 2; ++idx)
    25:             co_yield ++s_value;
    26:     }
    27: }

Recursion is activated when the parameter recurse is true, which is passed to recursiveCoro when initially called by main. It is then recursively called in line 13, now using false as its argument. Consider what happens when it's recursively called: the while-loop is entered and the for-statement at line 5 is executed, `co_yielding' two values. Next, in line 10, the loop ends, terminating the recursion. This implicitly calls co_return. It's also possible to do that explicitly, using

    if (not recurse)
        co_return;
Going back to the initial call: once rec (line 13) is available, a nested while-loop is entered (line 15), receiving the next value obtained by the recursive call (line 17). That next call resumes the nested coroutine, which, as just described, returns two values when executing line 5's for-statement. But then, when it's resumed for the third time, it doesn't actually co_yield a newly computed value, but calls co_return (because of lines 10 and 11), thus ending the recursive call. At that point the coroutine's State class's member done returns true, which value is available through ret.done() (line 18). Once that happens the while loop at line 15 ends, and the non-recursive coroutine continues at line 24. If the recursively called coroutine does compute a value, rec.done() returns false, and value produced by rec is `co_yielded' by the non-recursively called coroutine, making it available to main. So in that latter case the value co_yielded by the recursively called coroutine is co_yielded by the initially called coroutine, where it is retrieved by main: there's a sequence of co_yield statements from the most deeply nested coroutine to the coroutine that's called by main, at which point the value is finally collected in main.

The next..done implementation used here resembles the way streams are read: first try to extract information from a stream. If that succeeds, use the value; if not, do something else:

    while (true)
    {
        cin >> value;
        if (not cin)
            break;
        process(value);
    }
Functions like getline and overloaded extraction operators may combine the extraction and the test. That's of course also possible when using coroutines. Defining next as
    bool Recursive::next(size_t *value) 
    {
        d_handle.resume();
        if (d_handle.done())        // no more values
            return false;
        *value = d_handle.promise().value();
        return true;
    }
allows us to change the while loop at line 15 into:
    size_t value;
    while (rec.next(&value))
        co_yield value;

24.7.2: Beyond a single recursive call

In the introductory section the fiboCoro coroutine was presented. In this section the fiboCoro coroutine is going to be used by the recursiveCoro coroutine, using multiple levels of recursion.

To concentrate on the recursion process the fiboCoroutine's handler is defined in main as a global object, so it can directly be used by every recursive call of recursiveCoro. Here is the main function, using bool Recursive::next(size_t *value), and it also defines the global object g_fibo:

    Fibo g_fibo = fiboCoro();
    
    int main()
    {
        Recursive rec = recursiveCoro(0);
    
        size_t value;
        while (rec.next(&value))
        {
            cout << value << "\n"
                    "? ";
    
            string line;
            if (not getline(cin, line) or line == "q")
                break;
        }
    }

The Recursive class interface is identical to the one developed in the previous section, except for the Recursive::done member (which is not used anymore and was therefore removed from the interface), and changing next member's signature as shown. It's implementation was altered accordingly:

    bool Recursive::next(size_t *value)
    {
        d_handle.resume();
    
        if (d_handle.done())
            return false;
    
        *value = d_handle.promise().value();
        return true;
    }

In fact, the only thing that has to be modified to process deeper recursion levels is the recursiveCoro coroutine itself. Here is its modified version:

     1: Recursive recursiveCoro(size_t level)
     2: {
     3:     while (true)
     4:     {
     5:         for (size_t idx = 0; idx != 2; ++idx)
     6:             co_yield g_fibo.next();
     7: 
     8:         if (level < 5)
     9:         {
    10:             Recursive rec = recursiveCoro(level + 1);
    11:             size_t value;
    12:             while (rec.next(&value))
    13:                 co_yield value;
    14:         }
    15: 
    16:         for (size_t idx = 0; idx != 2; ++idx)
    17:             co_yield g_fibo.next();
    18: 
    19:         if (level > 0)
    20:             break;
    21:     }
    22: }

This implementation strongly resembles the 1-level recursive coroutine. Now multiple levels of recursion are allowed, and the maximum recursion level is set at 5. The coroutine knows its own recursion level via its size_t level parameter, and it recurses as long as level is less than 5 (line 8). At each level two series of two fibonacci values are computed (in the for-statements at lines 5 and 16). After the second for-statement the coroutine ends unless it's the coroutine that's called from main, in which case level is 0. The decision to end (recursively called) coroutines is made in line 19.

In this implementation the maximum recursion level is set to a fixed value. It's of course also possible that the coroutine itself decides that further recursion is pointless. Consider the situation where directory entries are examined, and where subdirectories are handled recursively. The recursive directory visiting coroutine might then have an implementation like this:

     1: Recursive recursiveCoro(string const &directory)
     2: {
     3:     chdir(directory.c_str());               // change to the directory
     4: 
     5:     while ((entry = nextEntry()))           // visit all its entries
     6:     {
     7:         string const &name = entry.name();
     8:         co_yield name;                      // yield the entry's name
     9: 
    10:         if (entry.type() == DIRECTORY)      // a directory?
    11:         {                                   // get the full path
    12:             string path = pathName(directory, name);
    13:             co_yield path;                  // yield the full path
    14: 
    15:             auto rec = recursiveCoro(path); // visit the entries of 
    16:             string next;                    // the subdir (and of its
    17:             while (rec.next(&next))         // subdirs)
    18:                 co_yield next;              // and yield them
    19:         }
    20:     }
    21: }

In this variant the (not implemented here) function nextEntry (line 5) produces all directory entries in sequence, and if an entry represents a directory (line 10), the same process is performed recursively (line 15), yielding its entries to the current coroutine's caller (line 18).

24.8: Coroutine iterators

The previous examples predominantly used while-statements to obtain the values returned by coroutines, many generic algorithms (as well as range-based for-loops) depend on the availability of begin and end members returning iterators.

Coroutines (or actually, their handling classes) may also define begin and end members returning iterators. In practice those iterators are input iterators (cf. section 18.2), providing access to the values co_yielded by their coroutines. Section 22.14 specifies their requirements. For plain types (like size_t which is co_yielded by Fibo::next) iterators should provide the following members:

The Iterator class is a value class. However, except for copy- and move-constructions, Iterator objects can only be constructed by Recursive's begin and end members. It has a private constructor and declares Recursive as its friend:

    class Iterator
    {
        friend bool operator==(Iterator const &lhs, Iterator const &rhs);
        friend class Recursive;

        Handle d_handle;

        public:
            Iterator &operator++();
            size_t operator*() const;

        private:
            Iterator(Handle handle);
    };

Iterator's constructor receives Recursive::d_handle, so it can use its own d_handle to control recursiveCoro's behavior:

    Recursive::Iterator::Iterator(Handle handle)
    :
        d_handle(handle)
    {}

The member Recursive::begin ensures that Iterator::operator* can immediately provide the next available value by resuming the coroutine. If that succeeds it passes d_handle to Iterator's constructor. If there are no values it returns 0, which is the Iterator that's also returned by Recursive::end:

    Recursive::Iterator Recursive::begin()
    {
        if (d_handle.promise().level() == 0)
            g_fibo.reset();
    
        d_handle.resume();
        return Iterator{ d_handle.done() ? 0 : d_handle };
    }
    
    Recursive::Iterator Recursive::end()
    {
        return Iterator{ 0 };
    }

The dereference operator simply calls and returns the value returned by State::value() and the prefix increment operator resumes the coroutine. If no value was produced it assigns 0 to its d_handle, resulting in true when compared to the iterator returned by Recursive::end:

    size_t Recursive::Iterator::operator*() const
    {
        return d_handle.promise().value();
    }
    
    Recursive::Iterator &Recursive::Iterator::operator++()
    {
        d_handle.resume();
    
        if (d_handle.done())
            d_handle = 0;
    
        return *this;
    }

24.9: Visiting directories using coroutines

Because coroutines are usually suspended once they have produced some intermediate but useful result they offer an alternative to stack-based approaches in which recursion is often used.

This section covers a coroutine that visits all elements of (nested) directories, listing all their path-names relative to the original starting directory. First a more traditional approach is covered, using a class having a member that recursively visits directory elements. Thereafter a coroutine is described performing the same job. Finally, some statistics about execution times of both approaches are discussed.

24.9.1: The `Dir' class showing directory entries

Here a class Dir is developed (recursively) showing all entries in and below a specified directory. The program defines a class Dir, used by main:
    int main(int argc, char **argv)
    {
        Dir dir{ argc == 1 ? "." : argv[1] };
    
        while (char const *entryPath = dir.entry())
            cout << entryPath << '\n';
    }

The class Dir, like the coroutine based implementation in the next section, uses the `dirent' C struct. As we prefer typenames starting with capitals, Dir specifies a simple using DirEntry = dirent so C's typename doesn't have to be used.

Dir defines just a few data members: d_dirPtr stores the pointer returned by C's function opendir; d_recursive points to a Dir entry that's used to handle a sub-directory of the current directory; d_entry is the name of the directory returned by Dir::entry member, which is refreshed at each call; d_path stores the name of the directory visited by a Dir object; and d_entryPath is d_entry's path name, starting at the initial directory name. Here is Dir's class interface:

    class Dir
    {
        using DirEntry = dirent;
    
        DIR *d_dirPtr = 0;
        Dir *d_recursive = 0;
    
        char const *d_entry;        // returned by entry()
        std::string d_path;         // Dir's directory name, ending in '/'
        std::string d_entryPath;
    
        public:
            Dir(char const *dir);   // dir: the name of the directory to visit
            ~Dir();
    
            char const *entry();
    };

Dir's constructor prepares its object for inspection of the entries of the directory whose name is received as its argument: it calls opendir for that directory, and prepares its d_path data member:

    Dir::Dir(char const *dir)
    :
        d_dirPtr(opendir(dir)),             // prepare the entries
        d_path( dir )
    {
        if (d_path.back() != '/')           // ensure that dirs end in '/'
            d_path += '/';
    }

Once a Dir object's lifetime ends its destructor simply calls closedir to return the memory allocated by opendir:

    inline Dir::~Dir()
    {
        closedir(d_dirPtr);
    }

The member entry performs two tasks: first, if a recursion is active then if a recursive entry is available, that entry is returned. Otherwise, if no recursive entry is available d_recursive's memory is deleted, and d_recursive is set to 0:

        // first part
    if (d_recursive)                                    // recursion active
    {
        if (char const *entry = d_recursive->entry())   // next entry
            return entry;                               // return it

        delete d_recursive;                             // delete the object
        d_recursive = 0;                                // end the recursion
    }

The second part is executed if there's no recursion or once all the recursive entries have been obtained. In that case all entries of the current directory are retrieved, skipping the two mere-dot entries. If the thus obtained entry is the name of a directory then d_recursive stores the address of a newly allocated Dir object (which is then handled at Dir::entry's next call) and the just received entry name is returned:

        // second part
    while (DirEntry const *dirEntry = readdir(d_dirPtr))// visit all entries
    {
        char const *name = dirEntry->d_name;            // get the name

        if (name == "."s or name == ".."s)              // ignore dot-names
            continue;

        name = (d_entryPath = d_path + name).c_str();   // entry-name
                                                        // (as path)

        if (dirEntry->d_type == DT_DIR)                 // a subdir?
            d_recursive = new Dir{ name };              // handle it next

        return name;                                    // return the entry
    }

The member Dir::entry itself consists of these two parts, returning zero (no more entries) once the second part's while-loop ends:

    char const *Dir::entry()
    {
        // first part here
    
        // second part here
    
        return 0;
    }

Thus, the class Dir essentially requires one single member function, using recursion to visit all directory entries that exist in or below the specified starting directory. All sources of this program are available in the distribution's yo/coroutines/demo/dir directory.

24.9.2: Visiting directories using coroutines

In this section a coroutine-based implementation of a program recursively showing all directory entries is discussed. The program was based on facilities offered by Lewis Baker's cppcoro library.

The source files of this program are available in the distribution's yo/coroutines/demo/corodir directory. It uses the same DirEntry type definition as used in the previous section, and specifies using Pair = std::pair<DirEntry, char const *> to access a DirEntry and its path name.

The program's main function strongly resembles the main function using the class Dir, but this time main uses the visitAllEntries coroutine:

    int main(int argc, char **argv)
    {
        char const *path = argc == 1 ? "." : argv[1];
    
        for (auto [entry, entryPath ]: visitAllEntries(path))
            cout << entryPath << '\n';
    }

The main function uses a range-based for-loop to show the entries produced by the visitAllEntries coroutine, which are the files and directories that are (recursively) found in a specified starting directory.

Three coroutines are used to process directories. The visitAllEntries coroutine returns a RecursiveGenerator<Pair> as its handler. Like main, the visitAllEntries coroutine also uses a range-based for-loop (line 3) to retrieve directory entries. The coroutine yields Pair objects (line 5) or the results from nested directories (line 9). Its handler (a RecursiveGenerator) is a class template, defined in Lewis Baker's cppcoro library:

     1: RecursiveGenerator<Pair> visitAllEntries(char const *path)
     2: {
     3:     for (auto &entry_pair: dirPathEntries(path))
     4:     {
     5:         co_yield entry_pair;
     6: 
     7:         auto [entry, entry_path] = entry_pair;
     8:         if (entry.d_type == DT_DIR)
     9:             co_yield visitAllEntries(entry_path);
    10:     }
    11: }

Directory entries are made available by a second coroutine, dirPathEntries. At each entry visitAllEntries is suspended (line 5), allowing main to show its full path. At lines 7 and 8 the types of the entries are inspected. If the received entry refers to a sub-directory then visitAllEntries yields, recursively calling itself, and thus yielding the sub-directory's entries. Once all entries have been processed the range-based for-loop ends, and the coroutine ends by automatically calling co_return.

The coroutine yielding directory entries is dirPathEntries, whose handler is an object of another cppcoro class, Generator<Pair>:

     1: Generator<Pair> dirPathEntries(char const *path)
     2: {
     3:     for (auto const &entry: dirEntries(path))
     4:         co_yield make_pair(entry,
     5:                     (string{path} + '/' + entry.d_name).c_str());
     6: }

The dirPathEntries coroutine performs a cosmetic task: it receives the path name of a directory, and calls a third coroutine (dirEntries) to retrieve the successive elements of that directory (line 3). As long as there are entries the coroutine is suspended, yielding Pair objects consisting of the values returned by dirEntry and the full path names of those entries (lines 4 and 5). Eventually, as with visitAllEntries, co_return ends the coroutine.

The third coroutine is dirEntries, returning a Generator<DirEntry handler:

     1: Generator<DirEntry> dirEntries(char const *path)
     2: {
     3:     DIR *dirPtr = opendir(path);
     4: 
     5:     while (auto entry = readdir(dirPtr))
     6:     {
     7:         if (accept(*entry))
     8:             co_yield *entry;
     9:     }
    10:     closedir(dirPtr);
    11: }

This coroutine, like the Dir class from the previous section, uses C's opendir, readdir, and closedir triplet of functions. As coroutines resume their actions beyond their suspension points these functions can now all be used in a single coroutine. When dirEntries starts, it calls opendir (line 3). Then, as long as there are entries (line 5) and those entries are neither the current nor the parent directory (line 7, checked by accept, not listed here), the coroutine is suspended, yielding the obtained entry (line 8). Its while-loop ends once all entries have been retrieved. At that point closedir is called (line 10), and the coroutine ends.

24.9.3: Functions vs. coroutines

Coroutines might be considered severe competitors of ordinary functions. After all, there's no repeated stack handling required between successive activations: co_yields simply suspend coroutines, leaving all their data, ready to be (re)used, in the heap.

In many situations coroutines are considered more attractive at an intuitive level: already in the introductory section of this chapter they were characterized as cooperating routines, where the coroutine cooperates with its caller, producing the caller's requested information as if the coroutine is part of the caller's code. But although it formally isn't it can conceptually be considered part of the caller's code, as it doesn't have to be called again and again during the caller's lifetime: once activated the coroutine remains available in memory and doesn't have to be called again when it's resumed. Coroutines are cooperating in that sense: they can in fact be considered part of the caller's code. In this way they really differ from independent functions which, when called repeatedly, are called again and again `from scratch', implying intensive stack operations.

On the other hand, using coroutines is way more complex than using `traditional' functions. The source files of the discussed Dir class required some 100 lines of source code, whereas the coroutine based implementation needed about 700 lines of code. But maybe that's not a fair comparison. Maybe the cppcoro library's sources shouldn't be considered, like when we're using --say-- strings, streams and vectors, in which case we also ignore the sizes of their sources when they're used in our programs. If we do ignore the sizes of cppcoro's sources, then the coroutine based implementation in fact requires fewer lines of code than the Dir class, as the Generator and RecursiveGenerator handler classes are provided by the cppcoro library.

Eventually, implementing parts of algorithms with coroutines, instead of using the (functions based) structured programming approach, might simply be a matter of taste. But maybe, as coroutines allow us to split up algorithms in separate parts which are not using stack-based activations, the efficiency of coroutine-based implementations exceeds the efficiency of implementations using separate supoprt functions. To get some information about the efficiency of programs using coroutines vs. programs that use separate support functions the class Dir based program and the coroDir based program was each run five times, processing a large multi-directory, multi-file structure, containing over 400.000 entries. The execution times of each of the five runs are highly comparable. The following table shows the average clock-times, the average user-times, and the average system-times for the class Dir based program and the coroDir based program:


time

real user system

class Dir   82 22 59
coroDir 88 25 62

Although the class Dir implementation uses slightly less time than the coroDir implementatin, the differences are small, and should not be interpreted as an indication that (maybe different from what was expected) coroutine based implementations are inherently slower than function based implementations. Furthermore, coroutines themselves often call ordinary functions (like readdir called by coroDir's coroutine dirEntries), which still require stack-handling.

The conclusion at the end of this chapter is therefore that, yes, coroutines are available in C++, but they require lots of effort before they can be used. Some libraries (like cppcoro) are available, but they're not (yet?) part of the software that comes standard with your C++ compiler. However, the underlying philosophy (being able to use cooperating routines) certainly is attractive, although it doesn't necessarily result in more efficient programs than programs which are developed using the traditional structured programming approach. And so, in the end, whether or not to use coroutines might simply boil down to a matter of taste.