co_await, co_yield, co_return în C++

Cuvintele cheie ale corutinelor (C++20): funcții care se pot opri la jumătate și pot fi reluate mai târziu de unde au rămas.

Sintaxă
co_yield valoare;      // produce o valoare și se suspendă
co_await expresie;     // așteaptă (se suspendă până la reluare)
co_return valoare;     // încheie corutina

Ce face co_await, co_yield, co_return

O funcție obișnuită rulează de la început până la return. O corutină se poate suspenda la jumătate și poate fi reluată mai târziu, cu toate variabilele locale păstrate. O funcție devine corutină dacă folosește oricare dintre co_await, co_yield sau co_return.

co_yield valoare produce o valoare și se oprește până când cine o folosește cere următoarea (generatori). co_await expresie suspendă corutina până când operația așteptată este gata (programare asincronă: rețea, fișiere). co_return încheie corutina, opțional cu un rezultat.

C++20 oferă doar mecanismul de bază (antetul <coroutine>): tipul returnat de corutină trebuie să aibă un promise_type care spune ce se întâmplă la pornire, la co_yield, la final. Exemplele de mai jos scriu un astfel de tip minimal. C++23 adaugă std::generator, dar biblioteca libc++ folosită aici nu îl are încă; în practică se folosesc și biblioteci ca cppcoro sau Boost.Asio.

Exemple

Exemplul 1: Generator cu co_yield

numara este o corutină: la fiecare co_yield se oprește și predă valoarea. urmator() o reia până la următorul co_yield. Bucla din corutină nu rulează „dintr-o dată”, ci pas cu pas, la cerere.

#include <coroutine>
#include <print>

struct Generator {
    struct promise_type {
        int curent = 0;
        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(int v) noexcept {
            curent = v;
            return {};
        }
        void return_void() noexcept {}
        void unhandled_exception() { throw; }
    };

    explicit Generator(std::coroutine_handle<promise_type> h) : h_(h) {}
    Generator(const Generator&) = delete;
    ~Generator() { h_.destroy(); }

    bool urmator() {
        h_.resume();
        return !h_.done();
    }
    int valoare() const { return h_.promise().curent; }

private:
    std::coroutine_handle<promise_type> h_;
};

Generator fibonacci(int cate) {
    int a = 0, b = 1;
    for (int i = 0; i < cate; ++i) {
        co_yield a;
        int urm = a + b;
        a = b;
        b = urm;
    }
}

int main() {
    Generator g = fibonacci(10);
    while (g.urmator()) {
        std::print("{} ", g.valoare());
    }
    std::println("");
}

Rezultat

0 1 1 2 3 5 8 13 21 34

Exemplul 2: Suspendare cu co_await și rezultat cu co_return

Mesajele arată ordinea reală: corutina pornește, se oprește la co_await, main face altceva, apoi o reia; corutina termină și trimite rezultatul cu co_return.

#include <coroutine>
#include <print>

struct Sarcina {
    struct promise_type {
        int rezultat = 0;
        Sarcina get_return_object() {
            return Sarcina{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_never initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_value(int v) noexcept { rezultat = v; }
        void unhandled_exception() { throw; }
    };

    explicit Sarcina(std::coroutine_handle<promise_type> h) : h_(h) {}
    Sarcina(const Sarcina&) = delete;
    ~Sarcina() { h_.destroy(); }

    void reia() { h_.resume(); }
    bool gata() const { return h_.done(); }
    int rezultat() const { return h_.promise().rezultat; }

private:
    std::coroutine_handle<promise_type> h_;
};

Sarcina calculeaza_comanda() {
    std::println("[corutină] pornesc calculul");
    co_await std::suspend_always{};
    std::println("[corutină] am fost reluată, termin");
    co_return 42;
}

int main() {
    Sarcina s = calculeaza_comanda();
    std::println("[main] corutina s-a oprit, gata = {}", s.gata());
    s.reia();
    std::println("[main] gata = {}, rezultat = {}", s.gata(), s.rezultat());
}

Rezultat

[corutină] pornesc calculul
[main] corutina s-a oprit, gata = false
[corutină] am fost reluată, termin
[main] gata = true, rezultat = 42

Greșeli frecvente

  • Folosești co_return sau co_yield într-o funcție care returnează un tip obișnuit (int, void). Tipul returnat trebuie să aibă un promise_type; altfel compilarea eșuează.
  • Folosești în corutină referințe la variabile care nu mai există când corutina este reluată (de exemplu parametri primiți prin referință de la un temporar). Corutina își păstrează doar propriile variabile locale; ce era prin referință poate dispărea între timp.
  • Uiți să distrugi corutina (handle.destroy()) când nu s-a terminat singură. Starea ei (variabilele locale) rămâne alocată: scurgere de memorie.

Comenzi înrudite

  • lambda ([](){}): O funcție fără nume, scrisă direct acolo unde e folosită; poate „captura” variabile din jur.
  • return: Încheie funcția curentă și trimite, opțional, o valoare înapoi celui care a apelat-o.
  • noexcept: Marchează o funcție care promite că nu aruncă excepții; ca operator, verifică la compilare dacă o expresie poate arunca.