Membuat generator python melalui c ++ 20 coroutines

Sep 27 2020

Katakanlah saya memiliki kode python ini:

def double_inputs():
    while True:
        x = yield
        yield x * 2
gen = double_inputs()
next(gen)
print(gen.send(1))

Ini mencetak "2", seperti yang diharapkan. Saya bisa membuat generator di c ++ 20 seperti itu:

#include <coroutine>

template <class T>
struct generator {
    struct promise_type;
    using coro_handle = std::coroutine_handle<promise_type>;

    struct promise_type {
        T current_value;
        auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        auto yield_value(T value) {
            current_value = value;
            return std::suspend_always{};
        }
    };

    bool next() { return coro ? (coro.resume(), !coro.done()) : false; }
    T value() { return coro.promise().current_value; }

    generator(generator const & rhs) = delete;
    generator(generator &&rhs)
        :coro(rhs.coro)
    {
        rhs.coro = nullptr;
    }
    ~generator() {
        if (coro)
            coro.destroy();
    }
private:
    generator(coro_handle h) : coro(h) {}
    coro_handle coro;
};

generator<char> hello(){
    //TODO:send string here via co_await, but HOW???
    std::string word = "hello world";
    for(auto &ch:word){
        co_yield ch;
    }
}

int main(int, char**) {
    for (auto i = hello(); i.next(); ) {
        std::cout << i.value() << ' ';
    }
}

Generator ini hanya menghasilkan string huruf demi huruf, tetapi string tersebut di-hardcode di dalamnya. Dalam python, dimungkinkan tidak hanya untuk menghasilkan sesuatu DARI generator tetapi juga menghasilkan sesuatu UNTUK itu. Saya percaya itu bisa dilakukan melalui co_await di C ++.

Saya membutuhkannya untuk bekerja seperti ini:

generator<char> hello(){
    std::string word = co_await producer; // Wait string from producer somehow 
    for(auto &ch:word){
        co_yield ch;
    }
}

int main(int, char**) {
    auto gen = hello(); //make consumer
    producer("hello world"); //produce string
    for (; gen.next(); ) {
        std::cout << gen.value() << ' '; //consume string letter by letter
    }
}

Bagaimana saya bisa mencapai itu? Bagaimana cara membuat "produser" ini menggunakan c ++ 20 coroutines?

Jawaban

12 NicolBolas Sep 27 2020 at 08:33

Pada dasarnya Anda memiliki dua masalah yang harus diatasi jika Anda ingin melakukan ini.

Yang pertama adalah bahwa C ++ adalah bahasa yang diketik secara statis. Artinya, jenis semua yang terlibat perlu diketahui pada waktu kompilasi. Inilah sebabnya mengapa generatortipe Anda harus berupa template, sehingga pengguna dapat menentukan tipe apa yang digiringnya dari coroutine ke pemanggil.

Jadi jika Anda ingin memiliki antarmuka dua arah ini, maka sesuatu pada hellofungsi Anda harus menentukan tipe keluaran dan jenis masukan.

Cara termudah untuk melakukannya adalah dengan membuat sebuah objek dan meneruskan non- constreferensi ke objek tersebut ke generator. Setiap kali melakukan a co_yield, pemanggil dapat memodifikasi objek yang direferensikan dan kemudian meminta nilai baru. Coroutine dapat membaca dari referensi dan melihat data yang diberikan.

Namun, jika Anda bersikeras untuk menggunakan tipe masa depan untuk coroutine sebagai keluaran dan masukan, maka Anda harus menyelesaikan masalah pertama (dengan membuat generatortemplat Anda OutputTypedan InputType) serta masalah kedua ini.

Lihat, tujuan Anda adalah mendapatkan nilai pada coroutine. Masalahnya adalah bahwa sumber dari nilai tersebut (fungsi yang memanggil coroutine Anda) memiliki objek masa depan. Tetapi coroutine tidak dapat mengakses objek masa depan. Ia juga tidak dapat mengakses objek promise yang direferensikan di masa mendatang.

Atau setidaknya, itu tidak bisa dilakukan dengan mudah.

Ada dua cara untuk melakukannya, dengan kasus penggunaan yang berbeda. Yang pertama memanipulasi mesin coroutine untuk menutup jalan menuju janji. Yang kedua memanipulasi properti co_yielduntuk melakukan hal yang pada dasarnya sama.

Mengubah

Objek promise untuk coroutine biasanya tersembunyi dan tidak dapat diakses dari coroutine. Ini dapat diakses oleh objek masa depan, yang dibuat janji dan yang bertindak sebagai antarmuka ke data yang dijanjikan. Tetapi itu juga dapat diakses pada bagian-bagian tertentu dari co_awaitmesin.

Secara khusus, saat Anda menjalankan co_awaitekspresi apa pun di coroutine, mesin akan melihat jenis promise Anda untuk melihat apakah ia memiliki fungsi yang dipanggil await_transform. Jika demikian, itu akan memanggil objek promise itu await_transformpada setiap ekspresi yang Anda gunakan co_await(setidaknya, dalam ekspresi co_awaityang Anda tulis secara langsung, bukan implisit menunggu, seperti yang dibuat oleh co_yield).

Karena itu, kita perlu melakukan dua hal: membuat overload await_transformpada jenis promise, dan membuat jenis yang tujuan utamanya adalah memungkinkan kita memanggil await_transformfungsi itu.

Sehingga akan terlihat seperti ini:

struct generator_input {};

...

//Within the promise type:
auto await_transform(generator_input);

Satu catatan singkat. Kelemahan dari penggunaan await_transformseperti ini adalah, dengan menetapkan bahkan satu kelebihan fungsi ini untuk promise kami, kami memengaruhi setiap co_await coroutine yang menggunakan jenis ini. Untuk generator coroutine, itu tidak terlalu penting, karena tidak ada banyak alasan co_awaitkecuali Anda melakukan peretasan seperti ini. Tetapi jika Anda membuat mekanisme yang lebih umum yang dapat dengan jelas menunggu sembarang menunggu sebagai bagian dari generasinya, Anda akan mendapat masalah.

OK, jadi kami memiliki await_transformfungsi ini ; apa yang perlu dilakukan fungsi ini? Itu perlu mengembalikan objek yang menunggu, karena co_awaitakan menunggu di atasnya. Tetapi tujuan dari objek yang menunggu ini adalah untuk mengirimkan referensi ke tipe input. Untungnya, mekanisme yang co_awaitdigunakan untuk mengubah awaitable menjadi nilai disediakan oleh metode awaitable await_resume. Jadi, kami bisa mengembalikan InputType&:

//Within the `generator<OutputType, InputType>`:
    struct passthru_value
    {
        InputType &ret_;

        bool await_ready() {return true;}
        void await_suspend(coro_handle) {}
        InputType &await_resume() { return ret_; }
    };


//Within the promise type:
auto await_transform(generator_input)
{
    return passthru_value{input_value}; //Where `input_value` is the `InputType` object stored by the promise.
}

Ini memberi akses coroutine ke nilai, dengan memanggil co_await generator_input{};. Perhatikan bahwa ini mengembalikan referensi ke objek.

The generatorjenis dapat dengan mudah dimodifikasi untuk memungkinkan kemampuan untuk memodifikasi InputTypeobjek yang tersimpan dalam janji. Cukup tambahkan sepasang sendfungsi untuk menimpa nilai input:

void send(const InputType &input)
{
    coro.promise().input_value = input;
} 

void send(InputType &&input)
{
    coro.promise().input_value = std::move(input);
} 

Ini mewakili mekanisme transportasi asimetris. Coroutine mengambil nilai di tempat dan waktu yang dipilihnya sendiri. Karena itu, tidak ada kewajiban nyata untuk segera menanggapi setiap perubahan. Ini bagus dalam beberapa hal, karena memungkinkan coroutine untuk mengisolasi dirinya dari perubahan yang merusak. Jika Anda menggunakan forloop berbasis rentang di atas wadah, wadah itu tidak dapat langsung dimodifikasi (dalam banyak cara) oleh dunia luar atau program Anda akan memamerkan UB. Jadi jika coroutine rapuh dengan cara itu, ia dapat menyalin data dari pengguna dan dengan demikian mencegah pengguna untuk memodifikasinya.

Secara keseluruhan, kode yang dibutuhkan tidak terlalu besar. Berikut adalah contoh kode Anda yang dapat dijalankan dengan modifikasi ini:

#include <coroutine>
#include <exception>
#include <string>
#include <iostream>

struct generator_input {};


template <typename OutputType, typename InputType>
struct generator {
    struct promise_type;
    using coro_handle = std::coroutine_handle<promise_type>;

    struct passthru_value
    {
        InputType &ret_;

        bool await_ready() {return true;}
        void await_suspend(coro_handle) {}
        InputType &await_resume() { return ret_; }
    };

    struct promise_type {
        OutputType current_value;
        InputType input_value;


        auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        auto yield_value(OutputType value) {
            current_value = value;
            return std::suspend_always{};
        }

        void return_void() {}

        auto await_transform(generator_input)
        {
            return passthru_value{input_value};
        }
    };

    bool next() { return coro ? (coro.resume(), !coro.done()) : false; }
    OutputType value() { return coro.promise().current_value; }

    void send(const InputType &input)
    {
        coro.promise().input_value = input;
    } 

    void send(InputType &&input)
    {
        coro.promise().input_value = std::move(input);
    } 

    generator(generator const & rhs) = delete;
    generator(generator &&rhs)
        :coro(rhs.coro)
    {
        rhs.coro = nullptr;
    }
    ~generator() {
        if (coro)
            coro.destroy();
    }
private:
    generator(coro_handle h) : coro(h) {}
    coro_handle coro;
};

generator<char, std::string> hello(){
    auto word = co_await generator_input{};

    for(auto &ch: word){
        co_yield ch;
    }
}

int main(int, char**)
{
    auto test = hello();
    test.send("hello world");

    while(test.next())
    {
        std::cout << test.value() << ' ';
    }
}

Lebih memberi hasil

Alternatif untuk menggunakan eksplisit co_awaitadalah dengan mengeksploitasi properti dari co_yield. Yaitu, co_yieldmerupakan ekspresi dan oleh karena itu memiliki nilai. Secara khusus, ini (kebanyakan) setara dengan co_await p.yield_value(e), di mana pobjek promise (ohh!) Dan eapa yang kami hasilkan.

Untungnya, kami sudah memiliki yield_valuefungsi; itu kembali std::suspend_always. Tetapi juga bisa kembali obyek yang selalu menunda, tetapi juga yang co_awaitbisa membongkar menjadi InputType&:

struct yield_thru
{
    InputType &ret_;

    bool await_ready() {return false;}
    void await_suspend(coro_handle) {}
    InputType &await_resume() { return ret_; }
};

...

//in the promise
auto yield_value(OutputType value) {
    current_value = value;
    return yield_thru{input_value};
}

Ini adalah mekanisme transpor simetris; untuk setiap nilai yang Anda hasilkan, Anda menerima nilai (yang mungkin sama seperti sebelumnya). Berbeda dengan co_awaitmetode eksplisit , Anda tidak dapat menerima nilai sebelum Anda mulai membuatnya. Ini bisa berguna untuk antarmuka tertentu.

Dan tentu saja, Anda dapat menggabungkannya sesuai keinginan Anda.