Sökresultat för

Vad är coroutines i C++

50 minuter i lästid
Jens Riboe
Jens Riboe
Senior/Expert Software Developer
Vad är coroutines i C++

Coroutines i C++20 ger dig ett språkstöd för kooperativ multitasking: funktioner som kan pausa och återupptas utan att du behöver bygga ett eget state machine-ramverk. I den här guiden bygger vi en minimal coroutine-“runtime” från grunden och visar hur de centrala byggstenarna (promise_type, coroutine_handle och awaiters) hänger ihop. Därefter går vi igenom fyra praktiska mönster: en task som kan stegas, en producer som returnerar ett slutresultat, en generator som yield:ar värden, samt en iterator variant av generator som kan användas i foreach-loopar.

Ett första inledande exempel

Här följer ett komplett körbart exempel på en task coroutine. I de följande avsnitten går jag igenom de olika delarna och hur de hänger ihop. Tankegången här är att du ser all programkod, så att du kan orientera dig i API beskrivningen som sedan följer.

#include <print>
#include <coroutine>

struct Suspend {
    constexpr bool await_ready() const noexcept { return false; }
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
};

struct [[nodiscard]] TaskCoroutine {
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    TaskCoroutine(handle_type h) : handle{h} {}
    ~TaskCoroutine() { if (handle) handle.destroy(); }

    bool has_more() {
        if (not handle || handle.done()) return false;
        handle.resume();
        return not handle.done();
    }

    TaskCoroutine(TaskCoroutine const&) = delete;
    TaskCoroutine(TaskCoroutine &&) noexcept = delete;
    auto operator=(TaskCoroutine const&) -> TaskCoroutine& = delete;
    auto operator=(TaskCoroutine &&) noexcept -> TaskCoroutine& = delete;
};

struct TaskCoroutine::promise_type {
    auto get_return_object() {
        std::println("      [promise] get_return_object");
        return TaskCoroutine{ handle_type::from_promise(*this) };
    }
    auto initial_suspend() noexcept {
        std::println("      [promise] initial_suspend");
        return Suspend{};
    }
    auto final_suspend() noexcept {
        std::println("      [promise] final_suspend");
        return Suspend{};
    }
    void return_void() {
        std::println("      [promise] return_void");
    }
    void unhandled_exception() { std::terminate(); }
};

auto SimpleTask(unsigned n) -> TaskCoroutine {
    std::println("      [coro] start");
    for (auto k = 1U; k <= n; ++k) {
        std::println("      [coro] {} / {} suspend", k, n);
        co_await Suspend{};
        std::println("      [coro] resumed");
    }
    std::println("      [coro] done");
}

int main() {
    std::println("[main] enter");
    auto t = SimpleTask(3);
    std::println("[main] task created");
    for (auto j = 1U; t.has_more(); ++j) {
        std::println("[main] loop {}", j);
    }
    std::println("[main] exit");
}

I programkoden ovan har vi, räknat uppifrån och ned:

  • Suspend - Ett suspenderings-objekt, mer formellt ett awaiter objekt.
  • TaskCoroutine - Ett coroutine-interface innehållande ett handtag till dess inre state och en funktion som väcker upp den så länge det finns mer data.
  • TaskCoroutine::promise_type - En promise-type klass, som representerar dess inre state.
  • SimpleTask - Själva coroutine definitionen.
  • main - Huvudprogrammet där vi skapar och anropar task objektet.

Kompilerar vi och kör programmet kan det se ut så här:

C++> g++ -std=c++23 -Wall ../simple-task.cxx -o task
C++> ./task 
[main] enter
      [promise] get_return_object
      [promise] initial_suspend
[main] task created
      [coro] start
      [coro] 1 / 3 suspend
[main] loop 1
      [coro] resumed
      [coro] 2 / 3 suspend
[main] loop 2
      [coro] resumed
      [coro] 3 / 3 suspend
[main] loop 3
      [coro] resumed
      [coro] done
      [promise] return_void
      [promise] final_suspend
[main] exit
C++> 

Jag har indenterat utskrifterna från coroutine objektet, så att man tydligt ser hur kontrollen flyttas från main till coroutine objektet och sen tillbaka. Detta upprepas tills det är färdigt.

Observera att vi pratar om C++20 coroutines i denna artikel, men jag kompilerar med C++23, eftersom jag gillar att kunna använda std::println() för utskrifterna.

Coroutine API

I detta avsnitt sammanfattar jag de viktigaste delarna av C++ coroutines API för enkel referens senare i artikeln och eventuella efterföljande artiklar om coroutines.

Coroutine Body

Trist nog så har man valt att inte införa någon ny syntax eller reserverat ord för att deklarera en coroutine, utan det sker på ett litet mer subtilt sätt. En (vanlig) funktion som innehåller något eller några av följande nya reserverade ord är hux flux en coroutine.

  • co_await
  • co_yield
  • co_return

Så, enklast tänkbara coroutine (som förvisso inte gör någonting alls) ser ut så här:

auto MyCoroutine() -> MyCoroutineInterface {
    co_return;
}

int main() {
    MyCoroutineInterface coro = MyCoroutine();
}

Coroutine Interface

En struct/klass blir ett coroutine-interface om den innehåller en medlemstyp med namnet promise_type och ett medlems-objekt av typen std::coroutine_handle<T>. Den förstnämnda representerar det interna tillståndet för en coroutine och den senare utgör en kapsel för den förstnämnda och handtag för applikations-koden. Både dessa diskuterar jag igenom i följande avsnitt.

Namnet på en dylik struct är valfritt, men vanligtvis har namnet ett suffix med någon av följande:

  • XyzCoroutine
  • XyzCoroutineInterface
  • XyzCoro

Det rekommenderas att annotera klassen med [[nodiscard]], vilket innebär att objekt av denna typ inte kan ignoreras av misstag, utan behöver tilldelas till en variabel, för senare bruk.

Vidare, så bör copy-members annoteras med delete för att undvika att kopiera ett coroutine objekt, vilket kan leda till odefinierat beteende. Om man inte deklarerar move-members, så blir även dessa implicit markerade med delete. Önskar man move-semantics, så går det förvisso bra att implementera dessa.

struct [[nodiscard]] MyCoroutineInterface {
    struct MyPromiseType; //forward decl
    using promise_type = MyPromiseType;
    using handle_type  = std::coroutine_handle<promise_type>;
    
    handle_type handle;
    MyCoroutineInterface(handle_type h) : handle(h) {}
    ~MyCoroutineInterface() { handle.destroy(); }
    
    MyCoroutineInterface(MyCoroutineInterface const&) = delete;
    auto operator=(MyCoroutineInterface const&) -> MyCoroutineInterface& = delete;
    MyCoroutineInterface(MyCoroutineInterface &&) = delete;
    auto operator=(MyCoroutineInterface &&) -> MyCoroutineInterface& = delete;
    //...
};

struct MyCoroutineInterface::MyPromiseType {
    //...
};

Coroutine Handle

Det interna tillståndet för en coroutine kapslas in i en std::coroutine_handle<PromiseType>. Ett objekt av denna, utgör gränsyta för att interagera med en coroutine från applikationen. I tabellen nedan visar jag det viktigaste funktionerna du bör känna till.

Name Returns Parameters Description
from_promise (static) coroutine_handle PromiseType& Creates a coroutine_handle from the promise object of a coroutine.
promise PromiseType& - Obtains a reference to the promise object.
from_address (static) coroutine_handle void* Creates a coroutine_handle from an underlying address of another coroutine_handle.
address void* - Returns the underlying address of the coroutine_handle.
resume - - Resumes the execution of the coroutine.
operator() - - Same as above
operator bool bool - Returns true if active. Same as address() != nullptr
done bool - Returns true if completed (i.e., at its final suspend point)
destroy - - Destroys the coroutine state of the coroutine

Promise Type

Varje coroutine-interface klass, måste ha en medlemstyp med det fixerade namnet promise_type. Detta kan vara en nästlad struct, eller ett typalias till en struct definierad på annan plats. Uppgiften för en promise-type är att hantera state, samt emitterade värden och/eller returnerat värde.

En källa till ständig irritation är att det finns ingen klass att använda eller ärva från eller dylikt. Man måste implementera en promise-type själv och missar man något så gnäller kompilatorn tjurigt. I tabellen nedan visar jag de viktigaste funktionerna du behöver känna till för att implementera en promise-type.

Name Returns Parameters Description
get_return_object CoroutineInterface - Initializes a coroutine_handle to the coroutine.
initial_suspend awaiter - Determines whether the coroutine should be suspended at its initial suspend point.
final_suspend awaiter - Determines whether the coroutine should be suspended at its final suspend point.
yield_value awaiter T Yields a value from the coroutine, via co_yield expr.
return_value - T Returns a final value from the coroutine, via co_return expr.
return_void - - Used when the coroutine returns nothing, or uses a plain co_return.
unhandled_exception - - Handles an exception in the coroutine, that should be propagated to the caller.

get_return_object

Funktionen get_return_object har som uppgift att skapa (och returnera) ett coroutine-interface objekt, vilket sen används av applikations-koden. Koden i funktionen ser i regel ut på följande vis:

auto get_return_object() -> CoroutineInterface {
    auto handle    = CoroutineHandle::from_promise(*this);
    auto interface = CoroutineInterface{handle};
    return interface;
}

Oftast kan koden kortas ned till en one-liner, på följande vis:

auto get_return_object() {
    return CoroutineInterface{ CoroutineHandle::from_promise(*this) };
}

Om konstruktorn till coroutine-interface klassen tar ett handle objekt och inte märkt med explicit, så räcker det med att returnera handle objektet:

struct CoroutineInterface {
    struct MyPromiseType {
        auto get_return_object() { return handle_type::from_promise(*this); }
        //...
    };
    
    using promise_type = MyPromiseType;
    using handle_type  = std::coroutine_handle<MyPromiseType>;
    handle_type handle;
    
    CoroutineInterface(handle_type h) : handle(h) {}
    //...
};

initial_suspend & final_suspend

Dessa två funktioner anropas i var sin ände av livs-cykeln för en coroutine. Den första direkt efter att coroutine-objektet skapats och avgör huruvida coroutine-objektet ska starta med att pausa eller inte. Den andra direkt innan coroutine-objektet avslutas, och avgör huruvida coroutine-objektet ska pausa eller inte innan det avslutas. initial_suspend kan användas för att utföra initialisering, och final_suspend kan användas för att utföra rensning eller avslutningsåtgärder.

Båda returnerar ett så kallat awaiter objekt, vilken signalerar till runtime-systemet hur det ska fortsätta. Detta objekt kan också konfigureras för att styra hur coroutine-objektet ska bete sig vid paus och återupptagning.

return_value vs. return_void

Beroende på om coroutine objektet ska returnera ett sista värde eller inte, så ska en av dessa return funktioner implementeras. Det blir ett kompileringsfel om båda är definierade.

Implementera return_void om coroutine-objektet inte ska returnera något sista värde. I de flesta fall gör den ingenting alls, utan fungerar som ett tips till kompilatorn att coroutine-objektet inte ska returnera något sista värde. För coroutine objektet, så kan den anropa co_return eller helt enkelt smita ut via sista klammerparentes. Båda varianterna betyder samma sak.

Implementera return_value om coroutine-objektet ska returnera ett sista värde. Detta innebär i regel följande:

  1. Deklarera en variabel i promise klassen, med önskad typ
  2. Låt return_value ta en parameter av denna typ och tilldela till variabeln ovan
  3. Deklarera en publik funktion som returnerar värdet av denna variabel (obs inte en referens eller pekare)
  4. Säkerställ att coroutine-objektet anropar co_return expr, där expr är av korrekt typ.
struct MyPromiseType {
    //...
    int returned_value;
    void return_value(int v) { returned_value = v; }
};

struct MyCoroutineInterface {
    //...
    handle_type h;
    int get_final_value() const { return h.promise().returned_value; }
};

auto Task() -> MyCoroutineInterface {
    //...
    co_return 42;
}

int main() {
    auto t = Task();
    //...
    auto v = t.get_final_value();
}

yield_value

Denna funktion hanterar emittering av ett värde via co_yield-instruktionen. Den tar ett argument av önskad typ och returnerar ett awaiter objekt som används för att styra paus och återupptagning av coroutine-objektet. Precis som för returvärdet, så behövs en variabel i promise objektet. Skillnaden är att denna uppdateras varje gång co_yield anropas.

struct MyPromiseType {
    //...
    int yielded_value;
    auto yield_value(int v) { yielded_value = v; return SuspendAwaiter{}; }
};

struct MyCoroutineInterface {
    //...
    handle_type h;
    int get_current_value() const { return h.promise().yielded_value; }
};

auto Task() -> MyCoroutineInterface {
    //...
    co_yield 42;
    //...
}

int main() {
    auto t = Task();
    //...
    auto v = t.get_current_value();
    //...
}

Awaiter

Ett awaiter objekt är en liten hjälpklass som används för att styra paus och återupptagning av en coroutine. Awaiter objekt returneras från funktioner som initial_suspend, final_suspend och yield_value i promise-type klassen. De kan också användas tillsammans med co_await operatorn.

Ett awaiter objekt måste implementera tre specifika funktioner som runtime-systemet anropar vid olika tillfällen. Precis som för promise_type, måste man själv hålla reda på funktionernas namn och parametrar.

Name Returns Parameters Description
await_ready bool - Returns true if the coroutine should continue without suspending.
await_suspend void coroutine_handle<> Called when the coroutine is about to suspend. Can be used to schedule work.
await_resume T - Called when the coroutine resumes. The return value becomes the result of co_await.

Här följer en mycket enkel awaiter, som signallerar att en coroutine ska suspenderas:

struct SuspendAwaiter {
    constexpr bool await_ready() const noexcept { return false; }
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
};

struct XyzCoroutine::promise_type {
    //...
    auto initial_suspend() noexcept { return SuspendAwaiter{}; }
}

Standardbiblioteket tillhandahåller två färdiga awaiter-typer, så man inte behöver skriva klassen ovan själv.

namespace std {
    struct suspend_always {
        constexpr bool await_ready() const noexcept { return false; }
        constexpr void await_suspend(coroutine_handle<>) const noexcept { }
        constexpr void await_resume() const noexcept { }
    };
    struct suspend_never {
        constexpr bool await_ready() const noexcept { return true; }
        constexpr void await_suspend(coroutine_handle<>) const noexcept { }
        constexpr void await_resume() const noexcept { }
    };
}
//...
struct XyzCoroutine::promise_type {
    //...
    auto initial_suspend() noexcept { return std::suspend_always{}; }
}

Deklarationsordning av promise och interface typer

Som du säkert redan noterat, så finns det ett cirkulärt beroende mellan promise- och interface typerna. Promise typerna är de som definierar hur en coroutine fungerar, medan interface typerna är de som används för att interagera med coroutines. Detta innebär att promise typerna måste definieras innan interface typerna kan användas, vilket kan leda till kompilatorfel om ordningen inte är korrekt. Det finns några olika varianter för hur de kan deklareras, vilka jag listar här nedan.

Variant 1

Forward deklarera promise-type, deklarera typalias för handle-type, samt definiera promise-type klassen.

struct XyzCoroutine {
    struct promise_type; //forward decl
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    XyzCoroutine(handle_type h) : handle(h) {}
    ~XyzCoroutine() { handle.destroy(); }
    
    struct promise_type {
        auto get_return_object() {
            return XyzCoroutine{ handle_type::from_promise(*this) };
        }
        //...
    };
    //...
};

Variant 2

Forward deklarera promise-type, deklarera typalias för handle-type, samt definiera promise-type klassen nedanför interface klassen.

struct XyzCoroutine {
    struct promise_type; //forward decl
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    XyzCoroutine(handle_type h) : handle(h) {}
    ~XyzCoroutine() { handle.destroy(); }
    //...
};

struct XyzCoroutine::promise_type {
    auto get_return_object() {
        return XyzCoroutine{ handle_type::from_promise(*this) };
    }
    //...
};

Variant 3

Definiera promise-type först och sen deklarera handle.

struct XyzCoroutine {
    struct promise_type {
        auto get_return_object() {
            return std::coroutine_handle<promise_type>::from_promise(*this);
        }
        //...
    };
  private:
    std::coroutine_handle<promise_type> handle;
  public:
    XyzCoroutine(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~XyzCoroutine() { handle.destroy(); }
};

Variant 4

Definiera promise-type som en generiskt template typ och använd den i interface klassen.

template<typename CoroInterface>
struct GenericPromise {
    auto get_return_object() {
        return std::coroutine_handle< GenericPromise<CoroInterface> >::from_promise(*this);
    }
    //...
};

struct XyzCoroutine {
    using promise_type = GenericPromise<XyzCoroutine>;
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    XyzCoroutine(handle_type h) : handle(h) {}
    ~XyzCoroutine() { handle.destroy(); }
    //...
};

Validering av handle_type

I coroutine destruktorn brukar man testa för att den är aktiv och om så är fallet anropa destroy().

//...
handle_type handle;
XyzCoro(handle_type h) : handle{h} {}
~XyzCoro {
    if (handle) handle.destroy();
}

Det som anropas i destruktorn är operator bool(), så det ser egentligen ut på följande sätt:

//...
~XyzCoro {
    if (handle.operator bool()) handle.destroy();
}

Denna boolska operator, testar om det finns ett coroutine objekt eller inte. Det vill säga det ser egentligen ut på följande sätt:

//...
~XyzCoro {
    if (handle.address() != nullptr) handle.destroy();
}

På liknande sätt kan vi översätta vad som sker i has_more() funktionen:

 bool has_more() {
    if (not handle || handle.done()) return false;
    //...
}        
 bool has_more() {
    if (not handle.operator bool() || handle.done()) return false;
    //...
}        
 bool has_more() {
    if (not (handle.address() != nullptr)) || handle.done()) return false;
    //...
}        
 bool has_more() {
    if (handle.address() == nullptr) || handle.done()) return false;
    //...
}        

När anropas promise-type funktionerna?

Om vi går tillbaka till vår task coroutine, som såg ut så här, när utskrifterna är borttagna:

auto SimpleTask(unsigned n) -> TaskCoroutine {
    for (auto k = 1U; k <= n; ++k) {
        co_await std::suspend_always{};
    }
    co_return;
}

Kompilatorn å andra sidan, kommer att betrakta denna funktion på följande (förenklade) sätt:

auto SimpleTask(unsigned n) -> TaskCoroutine {
    auto p = TaskCoroutine::promise_type{};
    auto coro = p.get_return_object();
    co_await p.initial_suspend();
    try {
        for (auto k = 1U; k <= n; ++k) {
            co_await std::suspend_always{};
        }
        p.return_void();
    } catch(...) {
        p.unhandled_exception();
    }
    co_await p.final_suspend();
}

Det är givetvis mer som händer bakom kulisserna, såsom att coro objektet ska föras över till anroparen, dvs main i vårt exempel. Men översättningen här ovan förmedlar en känsla för när respektive callback funktion i promise-type anropas.

Hur anropas funktionerna i en awaiter?

Givet att vi en programrad som ser ut som följande:

co_await std::suspend_always{};

Detta översätt till följande kodfragment:

auto&& a = std::suspend_always{};
if (not a.await_ready()) {
    a.await_suspend(std::coroutine_handle<>{/* current coroutine */});
    //leave coro -->
}
// --> back again
a.await_resume(); 

Simple Producer

En producer beräknar ett slutresultat, som sedan skickas ut via co_return. Så här ser en enkel sådan ut, som summerar loop-variabeln och returnerar på slutet.

#include <print>
#include <coroutine>

struct [[nodiscard]] ProducerCoroutine {
    struct promise_type {
        auto get_return_object() {
            return ProducerCoroutine{ handle_type::from_promise(*this) };
        }
         auto initial_suspend() noexcept { return std::suspend_always{}; }
         auto final_suspend()   noexcept { return std::suspend_always{}; }
         void unhandled_exception() {}

        unsigned returned_value = 0;
        void return_value(unsigned value) { returned_value = value; }
    };
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    ProducerCoroutine(handle_type h) : handle{h} {}
    ~ProducerCoroutine() { if (handle) handle.destroy(); }

    bool has_more() {
        if (not handle || handle.done()) return false;
        handle.resume();
        return not handle.done();
    }
    auto get_result() const {
        return handle.promise().returned_value;
    }

    ProducerCoroutine(ProducerCoroutine const&) = delete;
    auto operator=(ProducerCoroutine const&) -> ProducerCoroutine& = delete;
};

auto Producer(unsigned n) -> ProducerCoroutine {
    std::println("      [coro] start");
    auto sum = 0U;
    for (auto k = 1U; k <= n; ++k) {
        std::println("      [coro] {} / {} suspend", k, n);
        sum += k;
        co_await std::suspend_always{};
        std::println("      [coro] resumed");
    }
    std::println("      [coro] done");
    co_return sum;
}

int main() {
    std::println("[main] enter");
    auto p = Producer(3);
    std::println("[main] producer created");
    for (auto j = 1U; p.has_more(); ++j) {
        std::println("[main] loop {}", j);
    }
    std::println("[main] exit: sum={}", p.get_result());
}

Kompilerar och kör vi programmet kan det se ut så här:

C++> g++ -std=c++23 -Wall ../simple-producer.cxx -o producer
C++> ./producer 
[main] enter
[main] producer created
      [coro] start
      [coro] 1 / 3 suspend
[main] loop 1
      [coro] resumed
      [coro] 2 / 3 suspend
[main] loop 2
      [coro] resumed
      [coro] 3 / 3 suspend
[main] loop 3
      [coro] resumed
      [coro] done
[main] exit: sum=6
C++>

Simple Generator

En generator returnerar värden löpande via co_yield. Precis som för producer behövs en variabel som håller aktuellt värde. Denna tilldelas via API funktionen yield_value. Sen behöver man en applikations funktion som returnerar detta värde, på liknande sätt som för en producer.

#include <print>
#include <coroutine>

struct [[nodiscard]] GeneratorCoroutine {
    struct promise_type {
        auto get_return_object() {
            return GeneratorCoroutine{ handle_type::from_promise(*this) };
        }
        static auto initial_suspend() noexcept { return std::suspend_always{}; }
        static auto final_suspend()   noexcept { return std::suspend_always{}; }
        static void unhandled_exception() {}
        static void return_void() {}

        unsigned yielded_value = 0;
        auto yield_value(unsigned value) {
            yielded_value = value;
            return std::suspend_always{};
        }
    };
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    GeneratorCoroutine(handle_type h) : handle{h} {}
    ~GeneratorCoroutine() { if (handle) handle.destroy(); }

    bool has_more() {
        if (not handle || handle.done()) return false;
        handle.resume();
        return not handle.done();
    }
    auto get_current_value() const {
        return handle.promise().yielded_value;
    }

    GeneratorCoroutine(GeneratorCoroutine const&) = delete;
    auto operator=(GeneratorCoroutine const&) -> GeneratorCoroutine& = delete;
};

auto Generator(unsigned n) -> GeneratorCoroutine {
    std::println("      [coro] start");
    for (auto k = 1U; k <= n; ++k) {
        std::println("      [coro] {} / {} suspend", k, n);
        co_yield k;
        std::println("      [coro] resumed");
    }
    std::println("      [coro] done");
}

int main() {
    std::println("[main] enter");
    auto g = Generator(3);
    std::println("[main] generator created");
    while (g.has_more()) {
        std::println("[main] loop value={}", g.get_current_value());
    }
    std::println("[main] exit");
}

Så här kan det se ut om vi kompilerar och kör programmet:

C++> g++ -std=c++23 -Wall ../simple-generator.cxx -o generator
C++> ./generator 
[main] enter
[main] generator created
      [coro] start
      [coro] 1 / 3 suspend
[main] loop value=1
      [coro] resumed
      [coro] 2 / 3 suspend
[main] loop value=2
      [coro] resumed
      [coro] 3 / 3 suspend
[main] loop value=3
      [coro] resumed
      [coro] done
[main] exit
C++>

Simple Iterator

Det senaste exemplet kan vidareutvecklas lite till, så att den kan fungera i en vanlig iterator-kontext. Det som behövs är en nästlad iterator-klass, som kontrollerar coroutine objektet. Här kommer ett förslag på hur det kan se ut:

#include <coroutine>
#include <iterator>
#include <print>
#include <utility>

struct [[nodiscard]] IteratorCoroutine {
    struct promise_type {
        auto get_return_object() {
            return IteratorCoroutine{ handle_type::from_promise(*this) };
        }
        static auto initial_suspend() noexcept { return std::suspend_always{}; }
        static auto final_suspend()   noexcept { return std::suspend_always{}; }
        static void unhandled_exception() {}
        static void return_void() {}

        unsigned yielded_value = 0;
        auto yield_value(unsigned value) {
            yielded_value = value;
            return std::suspend_always{};
        }
    };
    using handle_type = std::coroutine_handle<promise_type>;
  private:
    handle_type handle;
  public:
    IteratorCoroutine(handle_type h) : handle{h} {}
    ~IteratorCoroutine() { if (handle) handle.destroy(); }
    IteratorCoroutine(IteratorCoroutine const&) = delete;
    auto operator=(IteratorCoroutine const&) -> IteratorCoroutine& = delete;

    struct Iterator {
        handle_type coroutine;
        Iterator(handle_type h_) : coroutine{h_} {}
        void operator++() { coroutine.resume(); }
        auto operator*() const {
            return coroutine.promise().yielded_value;
        }
        bool operator==(std::default_sentinel_t) const {
            return not coroutine || coroutine.done();
        }
    };

    auto begin() -> Iterator {
        if (handle) handle.resume();
        return Iterator{handle};
    }
    auto end() { return std::default_sentinel_t{}; }
};

auto Iterator(unsigned n) -> IteratorCoroutine {
    std::println("      [coro] start");
    for (auto k = 1U; k <= n; ++k) {
        std::println("      [coro] {} / {} suspend", k, n);
        co_yield k;
        std::println("      [coro] resumed");
    }
    std::println("      [coro] done");
}

int main() {
    std::println("[main] enter");
    usecase_while();
    usecase_foreach();
    usecase_fibonacci();
    std::println("[main] exit");
}

Funktionen main anropar tre användningsfall (usecase). Vi tar dem i tur och ordning, med funktionen följt av dess utdata.

Användningsfall: while

I detta första fall gör vi allting explicit, genom att skapa coroutine objektet, via den erhålla ett iterator objekt och iterera tills det tar slut och i varje varv hämta ut aktuellt värde.

void usecase_while() {
    std::println("-- while-loop --");
    auto coro = Iterator(3);
    auto iter = coro.begin();
    std::println("[main] iterator created");
    while (iter != coro.end()) {
        auto value = *iter;
        std::println("[main] value={}", value);
        ++iter;
    }
}
-- while-loop --
      [coro] start
      [coro] 1 / 3 suspend
[main] iterator created
[main] value=1
      [coro] resumed
      [coro] 2 / 3 suspend
[main] value=2
      [coro] resumed
      [coro] 3 / 3 suspend
[main] value=3
      [coro] resumed
      [coro] done

Användningsfall: foreach

I detta fall, använder vi foreach-loopen (range-based for-loop), som sköter alla mellanliggande ting.

void usecase_foreach() {
    std::println("-- foreach-loop --");
    for (auto k: Iterator(3)) std::println("[main] value={}", k);
}
-- foreach-loop --
      [coro] start
      [coro] 1 / 3 suspend
[main] value=1
      [coro] resumed
      [coro] 2 / 3 suspend
[main] value=2
      [coro] resumed
      [coro] 3 / 3 suspend
[main] value=3
      [coro] resumed
      [coro] done

Som synes, får vi precis samma utskrift som i föregående användningsfall.

Användningsfall: fibonacci

Till sist, visar jag hur man kan låta en lambda bli en coroutine. Detta fungerar eftersom en lambda översätts till en struct med en function-call operator, vilket är en vanlig funktion.

void usecase_fibonacci() {
    std::println("-- fibonacci --");
    auto fib = [](unsigned N) -> IteratorCoroutine {
        std::println("      [coro] start");
        auto f1 = 1U;
        auto f2 = 1U;
        for (auto k = 1U; k <= N; ++k) {
            std::println("      [coro] emit {}", f1);
            co_yield f1;
            f1 = std::exchange(f2, f1+f2);
        }
        std::println("      [coro] done");
    };
    for (auto k : fib(10)) std::println("[main] {}", k);
}
-- fibonacci --
      [coro] start
      [coro] emit 1
[main] 1
      [coro] emit 1
[main] 1
      [coro] emit 2
[main] 2
      [coro] emit 3
[main] 3
      [coro] emit 5
[main] 5
      [coro] emit 8
[main] 8
      [coro] emit 13
[main] 13
      [coro] emit 21
[main] 21
      [coro] emit 34
[main] 34
      [coro] emit 55
[main] 55
      [coro] done

Sammanfattning

C++20-coroutines är ett språkstöd för funktioner som kan pausa och återupptas, vilket gör det möjligt att skriva asynkrona och stegvisa flöden utan att manuellt bygga state machines. Artikeln visar hur en coroutine delas upp i ett “interface” (returtypen), ett “body” (själva funktionen med co_await/co_yield/co_return) och en promise_type som fungerar som bryggan mellan dem. Du får en konkret genomgång av std::coroutine_handle, hur initial_suspend och final_suspend styr när exekveringen startar och avslutas, samt hur en awaiter fungerar via await_ready, await_suspend och await_resume. Avslutningsvis byggs och jämförs ett antal olika praktiska coroutine idiom, vilka du kan använda som utgångspunkt för din egen kod.