Movatterモバイル変換


[0]ホーム

URL:




Chapter 24: Coroutines

Consider the following assignment: design a program that offers a functionnext returning the next fibonacci number at subsequentcalls. (Fibonacci numbers start with 0 and 1. The next fibonacci numberis the sum of the last two fibonacci numbers. The sequence, therefore, startswith 0, 1, 1, 2, 3, 5, etc. In this and the following examples fibonaccisequences are frequently used for illustration, and not because they areinherently related to coroutines.)

Here is an example of how such a program could be designed: it defines a classFibo and inmain Fibo::next is called to compute the next fibonaccinumber (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 thenext member isn't all that complicated. But when it's calledseveral actions are performed which themselves are unrelated to computingfibonacci numbers:

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

Nonetheless, in situations where the functions themselves are short and simple(likeFibo::next) these steps, requiring stack- and registermanipulations, might be considered unwelcome, raising the question whetherthey may be avoided.

C++coroutines allow us to avoid executing the steps that are requiredwhen calling ordinary functions. The upcoming sections cover coroutines indepth, but here is, for starters, the coroutine's equivalent ofFibo::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 thefiboCoro coroutine:

WhilefiboCoro lays the foundation for obtaining a sequence offibonacci numbers, resuming a coroutine doesn't mean calling a function theway the above memberFibo::next is called: there are no arguments; thereis no preparation of local variables; and there is no stackhandling. Instead there's merely a direct jump to the instruction just beyondthe coroutine's suspension point. When the coroutine's code encounters thenext suspension point (which occurs infiboCoro at it's next cycle,when it again reaches itsco_yield statement) then the program's executionsimply jumps back to the instruction following the instruction that resumedthe coroutine.

Themain function using the coroutine looks very similar to themainfunction using the classFibo:

     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 5fiboCoro is called, returning an (as yet not covered)fibo thing. Thisfibo thing is called acoroutine handler, and when (in line 14)fibo.next() is called, thenthat call resumesfiboCoro, which is then again suspended at itsco_yield statement, returning the next available value through thehandler'snext 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 nextinstruction to be executed, etc.). Normally, once suspended, the coroutine'scaller resumes its work beyond the instruction that returned the coroutine'snext value, so those two functions (the coroutine and its caller) closelycooperate to complete the implemented algorithm. Co-routines are thereforealso known ascooperating routines, which should not be confused withconcurrent (multi-threaded) routines.

How this whole process works, and what its characteristics are, is covered inthe 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 keywordsco_yield,co_await, orco_return. A coroutines cannot use thereturnkeyword, cannot define variadic parameters, and its return type must be anexisting type, which defines thecoroutine'shandler.

Although coroutines appear to return objects (as suggested by theFiboreturn type of theFibo fiboCoro() coroutine defined in theprevious section), in fact they do not. Instead coroutines returnso-called `handlers'. Such a handler isfibo, defined and used in theprevious section'smain function:

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

The classFibo itself defines the characteristics allowing the compiler togenerate code storing the coroutine's arguments, its local variables, thelocation of the next instruction to execute when the coroutine returns or issuspended, and its so-calledpromise_type object on the heap. This onlyhappens once, so when the coroutine is activated (as insum +=fibo.next()) the steps which are normally taken when a function is called areavoided, and instead the coroutine is immediately executed, using its alreadyavailable local variables and arguments. Coroutine's handler classes are sometimes calledFuture, and theirnested state classesmust be known as the handler'spromise_type. Thenamesfuture andpromise_type are completely unrelated to thestd::future (cf. section20.8) andstd::promise (cd. section20.12) types which are used in the context of multi threading. In fact,coroutines themselves are unrelated to multi threading, but are known ascooperating routines. Because the coroutines' handler and state classesare unrelated to thefuture andpromise classes used in the context ofmulti threading in this chapter the termsHandler andState aregenerally used.

It's one thing to define a coroutine, but when using a coroutine itshandler-class (theFibo class in the current example) must also bedefined. In addition, such a handler-classmust define anested class whose namemust be publicly available as the handler'spromise_type. The namepromise_type doesn't very well cover itspurpose, and using a more descriptive class name might be preferred. In thatcase a simple using declaration in the handler class's public section can beused, as shown in the following basic design of theFibo 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'sd_handle data member:

TheHandler's State class keeps track of the coroutine's state. Its basicelements are covered in the next section.

24.1.1: The coroutine's State class (promise_type)

The classHandler::State keeps track of the coroutine's state. It mustpublicly be known as the classHandler::promise_type, which can berealized using a public using-declaration associating a more appropriatelynamed class with `promise_type'.

In the current example the class nameState is used, having the followinginterface:

        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;        };

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

    Handler coro(int value, string const &str);
and theState class has a constructor
    Handler::State::State(int value, string const &str);
then that constructor is called.State's default constructor is calledif such a constructor is not available. In addition to callingState'sconstructor a coroutine can also use anawaiter to pass arguments to thehandler'sState class. This method is covered in section24.5.

The data memberd_value and member functionvalue() are specificallyused by the classFibo, and other coroutine state classes might declareother members. The remaining members are required, but the members returningstd::suspend_always could also be declaredas members returningstd::suspend_never.

By returning the (empty) structssuspend_always the coroutine's actionsare suspended until resumed. In practicesuspend_always is used, and sothe..._suspend members can be declaredstatic, using these basicimplementations:

    inline std::suspend_always Fibo::State::initial_suspend()     {         return {};     }    inline std::suspend_always Fibo::State::final_suspend() noexcept    {         return {};     }
Likewise, theunhandled_exception member can be declared static whenit 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) memberFibo::State::get_return_object returns anobject of the coroutine's handling class (so:Fibo). The recipe is:

Here isFibo::State::get_return_object's implementation:

    inline Fibo Fibo::State::get_return_object()    {        return Fibo{ std::coroutine_handle<State>::from_promise(*this) };    }
The memberFibo:State::yield_value can be overloaded for differentargument types. In ourFibo::State there's only oneyield_valuemember, storing its parameter value in theState::d_value data member. Italso suspends the coroutine's execution as it returnsstd::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 itsState subclass have beencovered, let's have a closer look at what happens when themain functionfrom the introductory section is executed. Here'smain 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 returningstd::suspend_always State's members returnstd::suspend_never? In that case the coroutine, once it has started, is isnever suspended. If the program computing fibonacci numbers is then calledwith argument 2, the following happens:

24.1.2: Simplifying the state class

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

Looking at theFibo::State class, its membersinitial_suspend,final_suspend andunhandled_exception are good candidates for such a baseclass. By defining the base class as a class template, receiving the coroutinehandler's class name and the handler's state class name as its template typeparameters then it can also provide the handler'sget_return_objectmember.

Here is how this base class can be defined. It is used by the coroutinehandler'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). Theycan also very well be defined as class members, in which case they have fullaccess to the members of their class.

In this section we develop a classFloats that can either write or read binaryfloat values to or from files. An object of thisclass is used inmain, calling its memberrun to either write or reada binary file (the full program is available in the distribution'scoroutines/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, orw forwriting, and the name of the binary file as its second argument.

The memberFloats:run uses pointers to members to call eitherread orwrite:

    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 memberread reads the binary file, using the coroutinecoRead. WhencoRead is called the usual actions are performed:implicitly the coroutineReader's State memberget_return_object iscalled to obtain the coroutine's handler, and the coroutine is suspended. Nextthe handler returned byget_return_object is made available as theread function'sreader 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 thereader object is available the memberread enters awhileloop repeatedly callingreader.next(). At this point the followinghappens:

When resumed for the first time (so whenreader.next() is called for thefirst time) thecoRead coroutine opens the file, and then, in awhile-statement, determines the next available value. If that succeeds thecoroutine is again suspended, usingco_yield to pass the just read valueon toread, or (if no value could be obtained) the coroutine ends bycallingco_return. Here is theFloats::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 memberwrite (re)writes the binary file, using the coroutinecoWrite, following the same procedure as used byread to obtain thewriter 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 memberFloats::coWrite behaves likeFloats::coRead, but writesinstead of reads values to the binary file. Here iscoWrite, defined as anormal (non-static) member, as it usesFloats::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;        }    }

TheReader andWriter handler classes are covered next.

24.2.1: The `Reader' coroutine handler

The essence of theReader class is that itsState subclass receives avalue from the coroutine (coRead) at the coroutine'sco_yieldstatement.Reader::State receives the value that's passed toco_yieldas argument of itsyield_value member, which stores the receivedfloatvalue in itsstd::optional<float> d_value data member.

TheReader class itself must define a constructor receiving a handle toitsState class , and should define a destructor. Itsnext membersimply returns the value that's stored in itsState class tonext'scaller. Here isReader'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 andReader::State's members have (except forReader::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, oncethe coroutine is again suspended (at itsco_yield statement), it returnsthe value stored in theReader::State object. It can access itsState class object viad_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

TheWriter class closely resembles theReader class. It uses adifferent value type, as it must writefloat values to the output streamusing their binary representations, but other than that there aren't that manydifference with theReader class. Here is its interface and theimplementations of itsyield_value member that differs from that of theReader 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 encounteredco_yield andco_return. What aboutco_await? The verbto await is more formal thanto wait, but thetwo verbs mean the same. The added level of formality ofto await isillustrated by a second description offered by theMerrian Websterdictionary:to remain in abeyance until, andabeyance's meaning takesus home again:a state of temporary inactivity orsuspension. So whenco_await is used the coroutine enters a state of temporary inactivity,i.e., it is suspended. In that senseco_yield is no different, as it alsosuspends the coroutine, but different fromco_yield co_await expects aso-calledawaitable expression. I.e., an expression resulting in anAwaitable, or that is convertible to anAwaitableobject (see also figure32).

Figure 32: co_await

Figure32 shows that the expression that's passed toco_await may be anAwaitable object, or if the coroutine handle'sState class has a memberawait_transform accepting an argument of someexpr's type, the value returned byawait_transform is theAwaitable (cf. figure33). Theseawait_transform membersmay be overloaded, so in any concrete situation severalAwaitable typescould be used.

Figure 33: awaitable

TheAwaiter type that's eventually used is either an object ofco_await's expr's type, or it is the return type of the (implicitly calledwhen defined) coroutine handler'sState::await_transform(expr) member.

Thus, theAwaitable object is amiddle-man, that's only used to obtainanAwaiter object. TheAwaiter is the real work-horse in the contextofco_await.

Awaitable classes may define a memberAwaiter Awaitable::operator co_await(), which may also be provided as afree function (Awaiter operator co_await(Awaitable &&)). If such aco_await conversion operator is available then it is used to obtain theAwaiter object from theAwaitable object. If such a conversionoperator isnot available then theAwaitable objectis theAwaiter object.

As an aside: the typesAwaitable andAwaiter are used here as formalclass names, and in actual programs the software engineer is free to use other(maybe more descriptive) names.

24.4: The class `Awaiter'

Once theAwaiter object is available its memberboolawait_ready() is called. If it returnstrue then the coroutine is notsuspended, but continues beyond itsco_await statement (in which casetheAwaitable object andAwaiter::await_ready were apparently able toavoid suspending the coroutine).

Ifawait_ready returnsfalseAwaiter::await_suspend(handle) is called. Itshandle argument is thehandle (e.g.,d_handle) of the current coroutine's handler object. Notethat at this point the coroutine has already been suspended, and thecoroutine's handle could even be transferred to another thread (in which casethe current thread must of course not be allowed to resume the currentcoroutine). The memberawait_suspend may returnvoid, bool, or somecoroutine's handle (optionally its own handle). As illustrated in figure34, when returningvoid ortrue the coroutine issuspended and the coroutine's caller continues its execution beyond thestatement that activated the coroutine. Iffalse is returned the coroutineis not suspended, and resumes beyond theco_await statement. If acoroutine's handle is returned (not a reference return type, but value returntype) then the coroutine whose handle is returned is resumed (assuming thatanother coroutine's handle is returned than the current coroutine issuspended, 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 automatonusing coroutines)

Figure 34: awaiter

If, followingawait_suspend, the current coroutine isagain resumed, then just before that theAwaiter object callsAwaiter::await_resume(), andawait_resume's return value is returnedby theco_await expression (await_resume) frequently defines avoid return type, as in

    static void Awaiter::await_resume() const    {}

In the next section a finite state automaton is implemented usingcoroutines. Their handler classes are alsoAwaiter types, withawait_ready returningfalse andawait_resume doing nothing. Thustheir definitions can be provided by a classAwaiter acting as base classof the coroutines' handler classes.Awaiter only needs a simple headerfile:

    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 ofits handler class. The handler class contains a subclass whose object keepstrack of the coroutine's state. In this chapter that subclass is namedState, and a using declaration is used to make it known aspromise_type which is required by the standard facilities made availablefor coroutines.

When coroutines are suspended atco_yield statements, the yielded valuesare passed toState class'syield_value members whose parametertypes match the types of the yielded values.

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

But let's assume that the coroutine performs a continuous loop containingseveral, maybe conditional,co_yield statements, and we want to inform theState class what the current iteration cycle is. In that case a parameteris less suitable, as tracking the cycle number is in fact a job for one of thelocal 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 upcommunication lines between coroutines and theState classes of theirHandler class objects. As an illustration, the originalfibocoro 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: }

TheAwaiter object, since there's noState::await_transform member,is an awaitable. Neither doesAwaiter have aType operatorco_await(), so the anonymousAwaiter object is indeed an Awaiter.

Being the Awaiter, it defines three members:await_ready, merely returningfalse, as the coroutine's execution must be suspended at theco_awaitstatement;await_suspend(handle), receiving a handle to the coroutine'sHandler's State object; andawait_resume, which doesn't have to doanything 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 memberawait_suspend uses the received handle to access theStateobject, passingcycle toState::setCycle:

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

In the next section (24.6) we useawait_suspend to switch fromone coroutine to another, but that's not required here. So the member returnsfalse, and thus continues its execution once it has passedcycle toState::setCycle. This way coroutines can pass information to theHandler's State object, which could define a data membersize_t const*d_cycle and a membersetCycle, usingd_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 viastate x inputmatrices. For example, when usingFlexc++ to recognize letters, digits orother characters it defines three input categories, and 4 states (the firststate being the INITIAL state determining the next state based on thecharacter read from the scanner's input stream, the other three being thestates that handle characters from their specific categories).

FSAs can also be implemented using coroutines. When using coroutines eachcoroutine handles a specific input category, and determines the category touse next, given the current input category. Figure35 shows a simpleFSA: atStart a digit takes us toDigit, a letter toLetter, atany other character we remain inStart, and at end of file (EOF) we endthe FSA at stateDone.Digit andLetter act analogously.

Figure 35: Finite State Automaton

This FSA uses four coroutines:coStart, coDigit, coLetter, andcoDone,each returning their own handlers (like aStart handler returned bycoStart, aDigit handler bycoDigit, etc.). Here iscoStart:

     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 theco_await statements at lines 9, 14, and 20: at these lines theco_awaits realize the switch from the current coroutine to another. Howthat's realized is soon described.

The coroutinescoDigit andcoLetter perform similar actions, butcoDone, called at EOF, simply returns, thus ending the coroutine-basedprocessing of the input stream. Here'scoDone, simply usingco_returnto 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, theStart handlerprivides a membergo starting the FSA by resuming its coroutine:

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

Themain function merely activates theStart coroutine, but thecoroutines might of course also be embedded in something like aclass FSA,andmain might offer an option to process a file argument instead of usingredirection. Here'smain:

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

24.6.1: The `Start' handler class

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

So, the nestedStart::State class only has to provide the standardmembers of the coroutine handler'sState class. As those are all providedby the genericPromiseBase class (section24.1.2)State needsno additional members:

                // nested under the Start handler class:    struct State: public PromiseBase<Start, State>     {};
Similar considerations apply to the other three handler classes: theirState classes are also derived fromPromiseBase<Handler,State>. However, as thecoDone coroutine also usesco_return, theDone::State state class must have its own areturn_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 fromDigit andLetter back toStart theStart handler class itself is an Awaiter (as areDigit,Letter, andDone). Section24.4 described the requirements andbasic definition of Awaiter classes.

From the point of view of FSAs the most interesting part is how to switch fromone coroutine to another. As illustrated in figure34 thisrequires a memberawait_suspend whichreceives the handle of thecoroutine using theco_await statement, and returnssome coroutine'shandle. So:

Here is the interface ofcoStart's handler class as well as the definitionof itsawait_suspend member. Since thecoStart coroutine may beresumed by several other coroutines it is unknown which coroutine's handle waspassed toStart::await_suspend, and soawait_suspend is a membertemplate, which simply returnsStart'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 memberStart's wait_suspend returnsStart's d_handle, thecoroutine containing theco_await g_start statement is suspended, and theco_start coroutine is resumed (see also figure34).

The implementations of theStart handler's constructor and destructor arestraightforward: the constructor stores the coroutine's handle in itsd_handle data member, the destructor uses the (language provided) memberdestroy to properly end theStart::State's coroutine handle. Here aretheir implementations:

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

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

24.6.2: Completing the Finite State Automaton

TheDigit andLetter coroutines handler classes are implementedlikeStart. LikecoStart, which continues its execution when anon-digit and non-letter character is received,coDigit continues for aslong as digit characters are received, andcoLetter continues for as longas letter characters are received.

As we've seen in section24.6 the implementation ofcoDone is abit different: it doesn't have to do anything, and thecoDone coroutinesimply ends at itsco_return statement. Coroutine execution (as well asthe FSA program) then ends followingmain's g_start.go() call.

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

24.7: Recursive coroutines

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

For starters, consider a very simple interactive program that produces aseries of numbers until the user ends the program or entersq:

     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 therecursiveCoro coroutine is called, returning its handlerrec. In line 7 its membernext is called, returning the next valueproduced byrecursiveCoro. The functionrecursiveCoro couldhave been any function returning an object of a class that has anextmember. 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 integralnumbers, starting at 0. Its two for-loops (lines 10 and 15) are there merelyfor illustrative purposes, and the recursive call will be placed between thosefor-loops. The variables_value is defined outside the coroutine (insteadof usingstatic s_value = 0 inside), as recursively called coroutines mustall access the sames_value variable. There's no magic here: just twofor-statements in a continuously iterating while-statement.

The interface of the returnedRecursive 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 itsState class are available inPromiseBase (cf. section24.1.2) and do not have to bemodified. AsrecursiveCoro co_yields values, theState::yield_value member stores those values in itsd_value data member:

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

Its membervalue is an accessor, returningd_value. When recursionis used the recursive calls end at some point. WhenrecursiveCorofunctions endState::return_void is called. It doesn't have to doanything, soPromiseBase's empty implementation perfectly does the job.

TheRecursive handling class's own interface starts at line 12. Itsd_handle data member (line 14) is initialized by its constructor (line19), which is all the constructor has to do. The handler's destructor only hasto calld_handle.destroy() to return the memory used by itsStateobject.

The remaining members arenext anddone. These, too, are implementedstraight-forwardly. The memberdone will shortly be used in the recursiveimplementation ofrecursiveCoro, and it just returns the value returned byd_handle.done().

When the membernext is called the coroutine is in its suspended state(which is what happens when it's initially called (cf. line 3 in the abovemain function) and thereafter when it usesco_yield (lines 11 and 16in the above implementation ofrecursiveCoro)). So it resumes thecoroutine, and when the coroutine is again suspended, it returns the (nextavailable) value stored in the handler'sState object:

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

24.7.1: Recursively calling recursiveCoro

Now we change the non-recursiverecursiveCoro coroutine into a recursivelycalled coroutine. To activate recursionrecursiveCoro is modified byadding 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 parameterrecurse istrue, which ispassed torecursiveCoro when initially called bymain. It is thenrecursively called in line 13, now usingfalse as its argument. Considerwhat happens when it's recursively called: the while-loop is entered and thefor-statement at line 5 is executed, `co_yielding' two values. Next, in line10, the loop ends, terminating the recursion. This implicitly callsco_return. It's also possible to do that explicitly, using

    if (not recurse)        co_return;
Going back to the initial call: oncerec (line 13) is available, anested while-loop is entered (line 15), receiving the next value obtained bythe recursive call (line 17). Thatnext call resumes the nested coroutine,which, as just described, returns two values when executing line 5'sfor-statement. But then, when it's resumed for the third time, it doesn'tactuallyco_yield a newly computed value, but callsco_return (becauseof lines 10 and 11), thusending the recursive call. At that point the coroutine'sState class's memberdone returnstrue, whichvalue is available throughret.done() (line 18). Once that happens thewhile loop at line 15 ends, and the non-recursive coroutine continues at line24. If the recursively called coroutinedoes compute a value,rec.done() returnsfalse, andvalue produced byrec is`co_yielded' by the non-recursively called coroutine, making it available tomain. So in that latter case the value co_yielded by the recursivelycalled coroutine is co_yielded by the initially called coroutine, where it isretrieved bymain: there's a sequence ofco_yield statements from themost deeply nested coroutine to the coroutine that's called bymain,at which point the value is finally collected inmain.

Thenext..done implementation used here resembles the way streams areread: first try to extract information from a stream. If that succeeds, usethe value; if not, do something else:

    while (true)    {        cin >> value;        if (not cin)            break;        process(value);    }
Functions likegetline and overloaded extraction operators may combinethe extraction and the test. That's of course also possible when usingcoroutines. Definingnext 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 thefiboCoro coroutine was presented. In this section thefiboCoro coroutine is going to be used by therecursiveCoro coroutine, using multiple levels of recursion.

To concentrate on the recursion process thefiboCoroutine's handler isdefined inmain as a global object, so it can directly be used by everyrecursive call ofrecursiveCoro. Here is themain function, usingbool Recursive::next(size_t *value), and it also defines the global objectg_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;        }    }

TheRecursive class interface is identical to the one developed in theprevious section, except for theRecursive::done member (which is not usedanymore and was therefore removed from the interface), and changingnextmember'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 recursionlevels is therecursiveCoro coroutine itself. Here is its modifiedversion:

     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. Nowmultiple levels of recursion are allowed, and the maximum recursion level isset at 5. The coroutine knows its own recursion level via itssize_t levelparameter, and it recurses as long aslevel is less than 5 (line 8). Ateach level two series of two fibonacci values are computed (in thefor-statements at lines 5 and 16). After the second for-statement thecoroutine ends unless it's the coroutine that's called frommain, in whichcaselevel is 0. The decision to end (recursively called) coroutines ismade in line 19.

In this implementation the maximum recursion level is set to a fixedvalue. It's of course also possible that the coroutine itself decides thatfurther recursion is pointless. Consider the situation where directory entries are examined, and where subdirectories are handledrecursively. The recursive directory visiting coroutine might then have animplementation 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) functionnextEntry (line 5)produces all directory entries in sequence, and if an entry represents adirectory (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 valuesreturned by coroutines, many generic algorithms (as well as range-basedfor-loops) depend on the availability ofbegin andend membersreturning iterators.

Coroutines (or actually, their handling classes) may also definebegin andend members returning iterators. In practice those iterators are inputiterators (cf. section18.2), providing access to the valuesco_yielded by their coroutines. Section22.14 specifies theirrequirements. For plain types (likesize_t which is co_yielded byFibo::next) iterators should provide the following members:

TheIterator class is a value class. However, except for copy- andmove-constructions,Iterator objects can only be constructed byRecursive's begin andend members. It has a private constructorand declaresRecursive 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 receivesRecursive::d_handle, so it can use its ownd_handle to controlrecursiveCoro's behavior:

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

The memberRecursive::begin ensures thatIterator::operator* canimmediately provide the next available value by resuming the coroutine. Ifthat succeeds it passesd_handle toIterator's constructor. Ifthere are no values it returns 0, which is theIterator that's alsoreturned byRecursive::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 byState::value() and the prefix increment operator resumes the coroutine. Ifno value was produced it assigns 0 to itsd_handle, resulting intruewhen compared to the iterator returned byRecursive::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 someintermediate but useful result they offer an alternative to stack-basedapproaches 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 startingdirectory. First a more traditional approach is covered, using a class havinga member that recursively visits directory elements. Thereafter acoroutine is described performing the same job. Finally, some statistics aboutexecution times of both approaches are discussed.

24.9.1: The `Dir' class showing directory entries

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

The classDir, like the coroutine based implementation in the nextsection, uses the`dirent'C struct. As we prefer typenames startingwith capitals,Dir specifies a simpleusing DirEntry = direntsoC's typename doesn't have to be used.

Dir defines just a few data members:d_dirPtr stores the pointer returned byC's functionopendir;d_recursive points to aDir entry that's used to handle a sub-directory of the current directory;d_entry is the name of the directory returned byDir::entry member, which is refreshed at each call;d_path stores the name of the directory visited by aDir object; andd_entryPath isd_entry's path name, starting at the initial directory name. Here isDir'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 thedirectory whose name is received as its argument: it callsopendir for that directory, and prepares itsd_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 aDir object's lifetime ends its destructor simply callsclosedir to return the memory allocated byopendir:

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

The memberentry performs two tasks: first, if a recursion is active thenif a recursive entry is available, that entry is returned. Otherwise, ifno recursive entry is availabled_recursive's memory is deleted, andd_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 recursiveentries have been obtained. In that case all entries of the current directoryare retrieved, skipping the two mere-dot entries. If the thus obtained entryis the name of a directory thend_recursive stores the address of a newlyallocatedDir object (which is then handled atDir::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 memberDir::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, theclass Dir essentially requires one single member function, usingrecursion to visit all directory entries that exist in or below the specifiedstarting directory. All sources of this program are available in thedistribution'syo/coroutines/demo/dir directory.

24.9.2: Visiting directories using coroutines

In this section a coroutine-based implementation of a program recursivelyshowing all directory entries is discussed. The program was based onfacilities offered by Lewis Baker'scppcoro library.

The source files of this program are available in the distribution'syo/coroutines/demo/corodir directory. It uses the sameDirEntrytype definition as used in the previous section, and specifiesusing Pair = std::pair<DirEntry, char const *> to access aDirEntry and its path name.

The program'smain function strongly resembles themain function usingthe classDir, but this timemain uses thevisitAllEntriescoroutine:

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

Themain function uses a range-based for-loop to show the entries producedby thevisitAllEntries coroutine, which are the files and directories thatare (recursively) found in a specified starting directory.

Three coroutines are used to process directories. ThevisitAllEntriescoroutine returns aRecursiveGenerator<Pair> as its handler. Likemain, thevisitAllEntries coroutine also uses a range-based for-loop(line 3) to retrieve directory entries. The coroutine yieldsPair objects(line 5) or the results from nested directories (line 9). Its handler (aRecursiveGenerator) is a class template, defined in Lewis Baker'scppcoro 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 entryvisitAllEntries is suspended (line 5),allowingmain to show its full path. At lines 7 and 8 the types of theentries are inspected. If the received entry refers to a sub-directory thenvisitAllEntries yields, recursively calling itself, and thus yielding thesub-directory's entries. Once all entries have been processed the range-basedfor-loop ends, and the coroutine ends by automatically callingco_return.

The coroutine yielding directory entries isdirPathEntries, whose handleris an object of anothercppcoro 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: }

ThedirPathEntries coroutine performs a cosmetic task: it receives thepath name of a directory, and calls a third coroutine (dirEntries) toretrieve the successive elements of that directory (line 3). As long as thereare entries the coroutine is suspended, yieldingPair objects consistingof the values returned bydirEntry and the full path names of thoseentries (lines 4 and 5). Eventually, as withvisitAllEntries, co_returnends the coroutine.

The third coroutine isdirEntries, returning aGenerator<DirEntryhandler:

     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 theDir class from the previous section, usesC'sopendir, readdir, andclosedir triplet of functions. Ascoroutines resume their actions beyond their suspension points these functionscan now all be used in a single coroutine. WhendirEntries starts, itcallsopendir (line 3). Then, as long as there are entries (line 5) andthose entries are neither the current nor the parent directory (line 7,checked byaccept, not listed here), the coroutine is suspended, yieldingthe obtained entry (line 8). Its while-loop ends once all entries have beenretrieved. At that pointclosedir is called (line 10), and the coroutineends.

24.9.3: Functions vs. coroutines

Coroutines might be considered severe competitors of ordinary functions. Afterall, there's no repeated stack handling required between successiveactivations: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 intuitivelevel: already in the introductory section of this chapter they werecharacterized ascooperating routines, where the coroutine cooperates withits caller, producing the caller's requested information as if the coroutineis part of the caller's code. But although it formally isn't itcanconceptually be considered part of the caller's code, as it doesn't have to becalled again and again during the caller's lifetime: once activated thecoroutine remains available in memory and doesn't have to be called again whenit's resumed. Coroutines arecooperating in that sense: they can in factbe considered part of the caller's code. In this way they really differ fromindependent functions which, when called repeatedly, are called again andagain `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 discussedDir classrequired some 100 lines of source code, whereas the coroutine basedimplementation needed about 700 lines of code. But maybe that's not a faircomparison. Maybe thecppcoro library's sources shouldn't be considered,like when we're using --say-- strings, streams and vectors, in which case wealso ignore the sizes of their sources when they're used in our programs. Ifwedo ignore the sizes ofcppcoro's sources, then the coroutine basedimplementation in fact requiresfewer lines of code than theDirclass, as theGenerator andRecursiveGenerator handler classes areprovided by thecppcoro library.

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

realusersystemclass Dir 822259coroDir882562

time



Although theclass Dir implementation uses slightly less time than thecoroDir implementatin, the differences are small, and should not beinterpreted as an indication that (maybe different from what was expected)coroutine based implementations are inherently slower than function basedimplementations. Furthermore, coroutines themselves often call ordinaryfunctions (likereaddir called bycoroDir's coroutinedirEntries), which still require stack-handling.

The conclusion at the end of this chapter is therefore that, yes, coroutinesare available inC++, but they require lots of effort before they can beused. Some libraries (likecppcoro) are available, but they're not (yet?)part of the software that comes standard with yourC++ compiler. However,the underlying philosophy (being able to use cooperating routines) certainlyis attractive, although it doesn't necessarily result in more efficientprograms than programs which are developed using the traditional structuredprogramming approach. And so, in the end, whether or not to use coroutinesmight simply boil down to a matter of taste.




[8]ページ先頭

©2009-2025 Movatter.jp