用c++20协同程序制作python生成器

2024-05-29 03:36:55 发布

您现在位置:Python中文网/ 问答频道 /正文

假设我有以下python代码:

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

它按预期打印“2”。 我可以用c++20制作这样的生成器:

#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() << ' ';
    }
}

这个生成器只是一个字母一个字母地生成一个字符串,但是字符串是硬编码的。在python中,不仅可以从生成器中生成某些内容,还可以向其生成某些内容。我相信它可以通过C++中的COEAWAIT来完成。p>

我需要它像这样工作:

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

我怎样才能做到这一点?如何使用c++20协同程序制作这个“生产者”


Tags: helloautostringreturnvaluegeneratornextgen
1条回答
网友
1楼 · 发布于 2024-05-29 03:36:55

如果你想这样做,你必须克服两个问题

首先,C++是一种静态类型的语言。这意味着需要在编译时知道所涉及的所有内容的类型。这就是为什么您的generator类型需要是一个模板,以便用户可以指定它从协同路由传递给调用方的类型

因此,如果您想拥有这个双向接口,那么hello函数上的something必须同时指定输出类型和输入类型

最简单的方法是创建一个对象,并将对该对象的非const引用传递给生成器。每次执行co_yield时,调用方都可以修改引用的对象,然后请求一个新值。协同程序可以从引用中读取并查看给定的数据

但是,如果您坚持使用coroutine的future类型作为输出和输入,那么您需要同时解决第一个问题(通过使generator模板采用OutputTypeInputType)以及第二个问题

看,您的目标是为协同程序获取一个值。问题是该值的源(调用协同程序的函数)有一个未来的对象。但是协同程序无法访问未来对象。它也不能访问将来引用的promise对象

或者至少,它不可能这么容易做到

有两种方法可以实现这一点,使用不同的用例。第一个操纵协同程序机器以后门方式进入承诺。第二个操作co_yield的属性来做基本相同的事情

转化

协同程序的promise对象通常是隐藏的,并且无法从协同程序访问。它可以被promise创建的未来对象访问,该对象充当承诺数据的接口。但在{}机器的某些部分也可以访问它

具体地说,当您对协同例程中的任何表达式执行co_await时,机器将查看您的承诺类型,以查看它是否具有名为await_transform的函数。如果是这样,它将调用promise对象的await_transformon每个co_await表达式(至少,在直接编写的co_await中,不是隐式等待,例如由co_yield创建的表达式)

因此,我们需要做两件事:在promise类型上创建一个await_transform重载,并创建一个类型,其唯一目的是允许我们调用该await_transform函数

所以看起来是这样的:

struct generator_input {};

...

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

一个简短的提示。像这样使用await_transform的缺点是,通过为我们的承诺指定此函数的一个重载,我们会影响使用此类型的任何协同例程中的每个co_await。对于生成器协同程序来说,这不是很重要,因为没有太多理由co_await,除非您正在进行这样的hack。但是,如果您正在创建一个更通用的机制,可以在生成过程中明确地等待任意的可等待项,那么您将遇到一个问题

好的,我们有这个await_transform函数;这个函数需要做什么?它需要返回一个可等待的对象,因为co_await将等待它。但是这个等待对象的目的是传递对输入类型的引用。幸运的是,co_await用于将等待值转换为值的机制是由等待值的await_resume方法提供的。所以我们可以返回一个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.
}

这通过调用co_await generator_input{};使协同程序能够访问该值。请注意,这将返回对对象的引用

可以很容易地修改generator类型,以允许修改promise中存储的InputType对象。只需添加一对send函数即可覆盖输入值:

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

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

这代表了一种不对称的传输机制。协同程序在自己选择的时间和地点检索值。因此,它不属于真正的义务您必须立即响应任何更改。这在某些方面是好的,因为它允许协同程序将自己与有害的变化隔离开来。如果您在容器上使用基于范围的for循环,那么外部世界无法直接修改该容器(在大多数情况下),否则您的程序将显示UB。因此,如果协同路由以这种方式脆弱,它可以从用户复制数据,从而防止用户修改数据

总之,所需的代码没有那么大。这里有一个run-able example of your code和这些修改:

#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() << ' ';
    }
}

更加屈服

使用显式co_await的替代方法是利用co_yield的属性。也就是说,co_yield是一个表达式,因此它有一个值。具体地说,它(大部分)相当于co_await p.yield_value(e),其中p是承诺对象(ohh!),而e是我们正在得到的

幸运的是,我们已经有了一个yield_value函数;它返回std::suspend_always。但是它也可以返回一个总是挂起的对象,但是,它co_await可以解压成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};
}

这是一种对称的传输机制;对于您产生的每个值,您都会收到一个值(可能与以前相同)。与显式co_await方法不同,在开始生成值之前,不能接收值。这对于某些接口可能很有用

当然,你可以根据自己的喜好把它们组合起来

相关问题 更多 >

    热门问题