Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can `co_yield` return a value from the caller on resumption of the coroutine?

C++20 introduces coroutines, which can be used to create generators and other similar things:

generator<int> counter(int max) {
    for(int i = 0; i < max; i++) {
        co_yield i;
    }
}

Is there any way to create a coroutine such that the caller can provide a response that is returned by co_yield once the coroutine is resumed? Let's call this a channel instead of a generator.

This is an example of what I want to be able to do:

channel<int, int> accumulator(int initial) {
    while(true) {
        // Can channel be written so co_yield provides a response?
        int response = co_yield initial;
        initial += response;
    }
}

Here, whenever the caller resumes the coroutine, it would provide a value which then gets returned from co_yield once the coroutine is resumed, like so:

std::vector<int> accumulate(std::vector<int> values) {
    channel<int, int> acc = accumulator(0);

    std::vector<int> summed_values;

    for(int v : values) {
        // Get whatever value was yielded by the accumulator
        int sum = acc.recieve();
        // Do something with the value
        summed_values.push_back(sum);
        // Resume the accumulator, returning this value from co_yield:
        acc.send(v); 
    }
    return summed_values;
}

Edit based on comment

Can anyone provide some guidance or an example on how to do this? Coroutines are still very new to me. I have a bare-bones implementation of a channel class, but I'm not sure what should be returned from yield_value in order to achieve this.

The two locations in question I've marked (A) and (B) in the comments.

template <class Out, class In>
struct channel {
    struct promise_type {
        Out current_value;
        auto yield_value(Out value) {
            current_value = value;
            // (A) What do I return here?
        }
        channel get_return_object() {
            return {std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        // We run up until the first value is ready
        auto initial_suspend() noexcept { return std::suspend_never(); }
        auto final_suspend() noexcept { return std::suspend_always(); }
        void unhandled_exception() noexcept { std::terminate(); }
    };


    Out receive() {
        return handle.promise().current_value;
    }
    void send(In response) {
        // (B) What do I do here?
    }
    // Constructors, destructor and move assignment operator omitted for brevity
   private:
    std::coroutine_handle<promise_type> handle = nullptr;
};
like image 621
Alecto Irene Perez Avatar asked Jun 13 '20 02:06

Alecto Irene Perez


1 Answers

The key is await_resume, which is called on the awaiter (result of yield_value) to obtain the result of co_yield.

You also need to store the response somewhere. As Raymond Chen suggested in comment, you can put the value in a new data member of promise_type.

So changes are:

  1. Add a data member to promise_type.

    In response;
    
  2. Define a customized awaiter to return that data member.

    struct awaiter : std::suspend_always {
        friend promise_type;
        constexpr In await_resume() const { return m_p->response; }
    
    private:
        constexpr awaiter(promise_type* p) : m_p(p) {}
        promise_type* m_p;
    };
    
  3. In (A), return the customized awaiter.

    return awaiter(this);
    
  4. In (B), set the data member, then resume the coroutine.

    handle.promise().response = response;
    handle.resume();
    
like image 172
cpplearner Avatar answered Sep 24 '22 14:09

cpplearner