通过c++20协程制作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++ 中的 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 协程来制作这个“生产者”?


幕布斯7119047
浏览 115回答 1
1回答

梦里花落0921

如果你想这样做,你基本上有两个问题需要克服。首先,C++ 是一种静态类型语言。这意味着在编译时需要知道所涉及的所有内容的类型。这就是为什么您的generator类型需要是模板,以便用户可以指定它从协程到调用者的引导类型。因此,如果您想拥有这个双向接口,那么您的函数中的某些内容hello必须指定输出类型和输入类型。最简单的方法是创建一个对象并将const对该对象的非引用传递给生成器。每次执行 a 时co_yield,调用者都可以修改引用的对象,然后请求新值。协程可以从引用中读取并查看给定的数据。但是,如果您坚持使用协程的未来类型作为输出和输入,那么您需要解决第一个问题(通过使模板generator采用OutputType和InputType)以及第二个问题。看,您的目标是为协程获取值。问题在于该值的来源(调用协程的函数)有一个 future 对象。但协程无法访问future 对象。它也无法访问 future 引用的 Promise 对象。或者至少,它不能那么容易做到。有两种方法可以根据不同的用例来实现此目的。第一个操纵协程机制,通过后门进入 Promise。第二个操作 的属性co_yield来执行基本相同的操作。转换协程的 Promise 对象通常是隐藏的并且无法从协程访问。它可以被 Promise 创建的 future 对象访问,并充当 Promise 数据的接口。但在机器的某些部分也可以访问它co_await。具体来说,当您对协程中的任何表达式执行 a 时co_await,机器会查看您的 Promise 类型以查看它是否具有名为 的函数await_transform。await_transform如果是这样,它将在您使用的每个表达式上调用该 Promise 对象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像这样使用的缺点是,通过为我们的 Promise 指定该函数的一个重载,我们会影响使用该类型的任何协程中的每个 co_await重载。co_await对于生成器协程来说,这并不是很重要,因为除非您进行这样的黑客攻击,否则没有太多理由这样做。但是,如果您正在创建一个更通用的机制,可以在其生成过程中明确等待任意可等待项,那么您就会遇到问题。OK,这样我们就有了这个await_transform功能;这个函数需要做什么?它需要返回一个可等待的对象,因为co_await它将等待它。但这个可等待对象的目的是传递对输入类型的引用。co_await幸运的是,用于将可等待转换为值的机制是由可等待的await_resume方法提供的。所以我们可以只返回一个InputType&://Within the `generator<OutputType, InputType>`:&nbsp; &nbsp; struct passthru_value&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; InputType &ret_;&nbsp; &nbsp; &nbsp; &nbsp; bool await_ready() {return true;}&nbsp; &nbsp; &nbsp; &nbsp; void await_suspend(coro_handle) {}&nbsp; &nbsp; &nbsp; &nbsp; InputType &await_resume() { return ret_; }&nbsp; &nbsp; };//Within the promise type:auto await_transform(generator_input){&nbsp; &nbsp; return passthru_value{input_value}; //Where `input_value` is the `InputType` object stored by the promise.}这使协程可以通过调用来访问该值co_await generator_input{};。请注意,这将返回对该对象的引用。该generator类型可以轻松修改,以允许修改InputType存储在 Promise 中的对象。只需添加一对send函数即可覆盖输入值:void send(const InputType &input){&nbsp; &nbsp; coro.promise().input_value = input;}&nbsp;void send(InputType &&input){&nbsp; &nbsp; coro.promise().input_value = std::move(input);}&nbsp;这代表了一种不对称的传输机制。协程在自己选择的地点和时间检索值。因此,它没有真正的义务立即响应任何变化。这在某些方面是好的,因为它允许协程将自身与有害的更改隔离开来。如果您在容器上使用基于范围的for循环,则该容器不能被外界直接修改(以大多数方式),否则您的程序将出现 UB。因此,如果协程在这种情况下很脆弱,它可以从用户那里复制数据,从而阻止用户修改它。总而言之,所需的代码并没有那么大。下面是经过这些修改的代码的可运行示例:#include <coroutine>#include <exception>#include <string>#include <iostream>struct generator_input {};template <typename OutputType, typename InputType>struct generator {&nbsp; &nbsp; struct promise_type;&nbsp; &nbsp; using coro_handle = std::coroutine_handle<promise_type>;&nbsp; &nbsp; struct passthru_value&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; InputType &ret_;&nbsp; &nbsp; &nbsp; &nbsp; bool await_ready() {return true;}&nbsp; &nbsp; &nbsp; &nbsp; void await_suspend(coro_handle) {}&nbsp; &nbsp; &nbsp; &nbsp; InputType &await_resume() { return ret_; }&nbsp; &nbsp; };&nbsp; &nbsp; struct promise_type {&nbsp; &nbsp; &nbsp; &nbsp; OutputType current_value;&nbsp; &nbsp; &nbsp; &nbsp; InputType input_value;&nbsp; &nbsp; &nbsp; &nbsp; auto get_return_object() { return generator{coro_handle::from_promise(*this)}; }&nbsp; &nbsp; &nbsp; &nbsp; auto initial_suspend() { return std::suspend_always{}; }&nbsp; &nbsp; &nbsp; &nbsp; auto final_suspend() { return std::suspend_always{}; }&nbsp; &nbsp; &nbsp; &nbsp; void unhandled_exception() { std::terminate(); }&nbsp; &nbsp; &nbsp; &nbsp; auto yield_value(OutputType value) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; current_value = value;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return std::suspend_always{};&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; void return_void() {}&nbsp; &nbsp; &nbsp; &nbsp; auto await_transform(generator_input)&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return passthru_value{input_value};&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; };&nbsp; &nbsp; bool next() { return coro ? (coro.resume(), !coro.done()) : false; }&nbsp; &nbsp; OutputType value() { return coro.promise().current_value; }&nbsp; &nbsp; void send(const InputType &input)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; coro.promise().input_value = input;&nbsp; &nbsp; }&nbsp;&nbsp; &nbsp; void send(InputType &&input)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; coro.promise().input_value = std::move(input);&nbsp; &nbsp; }&nbsp;&nbsp; &nbsp; generator(generator const & rhs) = delete;&nbsp; &nbsp; generator(generator &&rhs)&nbsp; &nbsp; &nbsp; &nbsp; :coro(rhs.coro)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; rhs.coro = nullptr;&nbsp; &nbsp; }&nbsp; &nbsp; ~generator() {&nbsp; &nbsp; &nbsp; &nbsp; if (coro)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; coro.destroy();&nbsp; &nbsp; }private:&nbsp; &nbsp; generator(coro_handle h) : coro(h) {}&nbsp; &nbsp; coro_handle coro;};generator<char, std::string> hello(){&nbsp; &nbsp; auto word = co_await generator_input{};&nbsp; &nbsp; for(auto &ch: word){&nbsp; &nbsp; &nbsp; &nbsp; co_yield ch;&nbsp; &nbsp; }}int main(int, char**){&nbsp; &nbsp; auto test = hello();&nbsp; &nbsp; test.send("hello world");&nbsp; &nbsp; while(test.next())&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; std::cout << test.value() << ' ';&nbsp; &nbsp; }}变得更有收获使用显式的替代方法co_await是利用 的属性co_yield。即,co_yield是一个表达式,因此它有一个值。具体来说,它(大部分)相当于co_await p.yield_value(e)Promisep对象(哦!),并且e是我们要产生的对象。幸运的是,我们已经有了一个yield_value函数;它返回std::suspend_always。但它也可以返回一个始终挂起的对象,但也可以将其co_await解包为InputType&:struct yield_thru{&nbsp; &nbsp; InputType &ret_;&nbsp; &nbsp; bool await_ready() {return false;}&nbsp; &nbsp; void await_suspend(coro_handle) {}&nbsp; &nbsp; InputType &await_resume() { return ret_; }};...//in the promiseauto yield_value(OutputType value) {&nbsp; &nbsp; current_value = value;&nbsp; &nbsp; return yield_thru{input_value};}这是一种对称传输机制;对于您产生的每一个值,您都会收到一个值(可能与以前的值相同)。与显式方法不同,在开始生成它们之前co_await您无法接收值。这对于某些接口可能很有用。当然,您可以根据需要将它们组合起来。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Python