【问题标题】:Making python generator via c++20 coroutines通过c++20协程制作python生成器
【发布时间】:2021-04-30 05:03:33
【问题描述】:

假设我有这个 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++ 中的 co_await 来完成。

我需要它像这样工作:

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协程制作这个“生产者”?

【问题讨论】:

  • 为什么要这样?直接将这个“生产者”传递给hello 不是更有意义吗?我的意思是,您可能可以使用co_await 恶作剧来做到这一点,但是当最明显的方法(将其传递给生产者)更明显时,为什么要使用这种机制呢? C++ 没有使用协程来尝试将其转换为 Python。

标签: python c++ yield c++20 c++-coroutine


【解决方案1】:

如果你想这样做,基本上有两个问题需要克服。

首先,C++ 是一种静态类型语言。这意味着需要在编译时知道所涉及的所有内容的类型。这就是为什么你的generator 类型需要是一个模板,以便用户可以指定它将什么类型从协程引导到调用者。

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

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

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

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

或者至少,它不是那么容易做到的。

有两种方法可以解决这个问题,不同的用例。第一个操作协程机制以通过后门进入 Promise。第二个操作co_yield 的属性来做基本相同的事情。

变换

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

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

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

所以看起来像这样:

struct generator_input {};

...

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

一个简短的说明。像这样使用 await_transform 的缺点是,通过为我们的 promise 指定这个函数的一个重载,我们会在任何使用这种类型的协程中影响 每个 co_await。对于生成器协程,这不是很重要,因为没有太多理由 co_await 除非你正在做这样的 hack。但是,如果您正在创建一个更通用的机制,可以明显地等待任意可等待对象作为其生成的一部分,那么您就会遇到问题。

好的,所以我们有了这个await_transform 函数;这个功能需要做什么?它需要返回一个等待对象,因为co_await 将等待它。但是这个等待对象的目的是提供对输入类型的引用。幸运的是,co_await 用于将 awaitable 转换为值的机制由 awaitable 的 await_resume 方法提供。所以我们可以只返回一个InputType&amp;

//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 是promise 对象(哦!),e 是我们正在生成的对象。

幸运的是,我们已经有了yield_value 函数;它返回std::suspend_always。但它也可以返回一个始终挂起的对象,但 co_await 可以解压成 InputType&amp;

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 方法不同,您不能在开始生成它们之前接收值。这对于某些接口可能很有用。

当然,您可以根据需要将它们组合起来。

【讨论】:

  • 你是魔术师,这正是我需要的!非常感谢,如果没有你的帖子,我不会在几个世纪内得到它。
  • 太好了,这个机制是否以某种方式实现,或者可以通过cppcoro 轻松实现吗?
  • @ClaasBontus:“这个机制是否以某种方式实现了”我不知道你所说的“机制”被“实现”是什么意思。如果您谈论的是await_transformyield_value,那么这些都是C++20 的一部分。而且我不知道“cppcoro”提供了什么。
猜你喜欢
  • 2017-09-02
  • 2011-08-22
  • 2020-02-26
  • 1970-01-01
  • 2015-10-31
  • 2020-06-20
  • 2013-08-02
  • 2015-12-11
相关资源
最近更新 更多