A Generic Data Stream with Coroutines in C++20

A Generic Data Stream with Coroutines in C++20

This post is a cross-post from www.ModernesCpp.com.

In my last post in this mini-series to coroutines from the practical perspective, I presented the workflow of "An Infinite Data Stream with Coroutines in C++20". In this post, I use the generic potential of the data stream.

This post assumes that you know the previous post "An Infinite Data Stream with Coroutines in C++20", in which I explain very detailed the workflow of an infinite generator, based on the new keyword co_yield So far, I have written about the new keywords co_return, and co_yield, which makes out of a function a coroutine. In the next post, I will have a closer look at the most challenging new keyword co_await.

co_return:

co_yield:

 Finally, to something new.

Generalization of the Generator

You may wonder why I never used the full generic potential of Generator in my last post. Let me adjust its implementation to produce the successive elements of an arbitrary container of the Standard Template Library.

// coroutineGetElements.cpp

#include <coroutine>
#include <memory>
#include <iostream>
#include <string>
#include <vector>

template<typename T>
struct Generator {
    
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
    
    Generator(handle_type h): coro(h) {}                      

    handle_type coro;
    
    ~Generator() {  
        if ( coro ) coro.destroy();
    }
    Generator(const Generator&) = delete;
    Generator& operator = (const Generator&) = delete;
    Generator(Generator&& oth): coro(oth.coro) {
        oth.coro = nullptr;
    }
    Generator& operator = (Generator&& oth) {
        coro = oth.coro;
        oth.coro = nullptr;
        return *this;
    }
    T getNextValue() {
        coro.resume();
        return coro.promise().current_value;
    }
    struct promise_type {
        promise_type() {}                              
          
        ~promise_type() {}
        
        std::suspend_always initial_suspend() {            
            return {};
        }
        std::suspend_always final_suspend() noexcept {
            return {};
        }
        auto get_return_object() {      
            return Generator{handle_type::from_promise(*this)};
        }
      
        std::suspend_always yield_value(const T value) {    
            current_value = value;
            return {};
        }
         void return_void() {}
        void unhandled_exception() {
            std::exit(1);
        }

        T current_value;
    };

};

template <typename Cont>
Generator<typename Cont::value_type> getNext(Cont cont) {
    for (auto c: cont) co_yield c;
}

int main() {

    std::cout << '\n';
  
    std::string helloWorld = "Hello world";
    auto gen = getNext(helloWorld);                        // (1)
    for (int i = 0; i < helloWorld.size(); ++i) {
        std::cout << gen.getNextValue() << " ";            // (4)
    }

    std::cout << "\n\n";

    auto gen2 = getNext(helloWorld);                       // (2)
    for (int i = 0; i < 5 ; ++i) {                         // (5)
        std::cout << gen2.getNextValue() << " ";
    }

    std::cout << "\n\n";

    std::vector myVec{1, 2, 3, 4 ,5};
    auto gen3 = getNext(myVec);                           // (3)
    for (int i = 0; i < myVec.size() ; ++i) {             // (6)
        std::cout << gen3.getNextValue() << " ";
    }
    
    std::cout << '\n';

}

In this example, the generator is instantiated and used three times. In the first two cases, gen (line 1) and gen2 (line 2) are initialized with std::string helloWorld, while gen3 uses a std::vector<int> (line 3). The output of the program should not be surprising. Line 4 returns all characters of the string helloWorld successively, line 5 only the first five characters, and line 6 the elements of the std::vector<int>.

You can try out the program on the Compiler Explorer.

No alt text provided for this image

To make it short. The implementation of the Generator<T> is almost identical to the previous one in the post An Infinite Data Stream with Coroutines in C++20. The crucial difference with the previous program is the coroutine getNext.

template <typename Cont>
Generator<typename Cont::value_type> getNext(Cont cont) {
    for (auto c: cont) co_yield c;
}

getNext is a function template that takes a container as an argument and iterates in a range-based for loop through all elements of the container. After each iteration, the function template pauses. The return type Generator<typename Cont::value_type> may look surprising to you. Cont::value_type is a dependent template parameter, for which the parser needs a hint. By default, the compiler assumes a non-type if it could be interpreted as a type or a non-type. For this reason, I have to put typename in front of Cont::value_type.

The Workflows

The compiler transforms your coroutine and runs two workflows: the outer promise workflow and the inner awaiter workflow.

The Promise Workflow

So far, I have only written about the outer workflow, which is based on the member functions of the promise_type.

{
    Promise prom;
    co_await prom.initial_suspend();
    try {
        <function body having co_return, co_yield, or co_wait>
    }
    catch (...) {
        prom.unhandled_exception();
    }
FinalSuspend:
    co_await prom.final_suspend();
}

When you followed my previous post, this workflow should look familiar to you. You already know the components of this workflow such as prom.initial_suspend(), the function body, and prom.final_suspend().

The Awaiter Workflow

The outer workflow is base on the Awaitables, which return Awaiters. I intentionally simplified this explanation. You already know two predefined Awaitables:

  • std::suspend_always
struct suspend_always {
    constexpr bool await_ready() const noexcept { return false; }
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
};
  • std::suspend_never
struct suspend_never {
    constexpr bool await_ready() const noexcept { return true; }
    constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
};

No, you may already guess on which parts the awaiter workflow is based on? Right! On the member functions await_ready(), await_suspend(), and await_resume() of the Awaitable.

awaitable.await_ready() returns false:
    
    suspend coroutine
	
    awaitable.await_suspend(coroutineHandle) returns: 
	
        void:
            awaitable.await_suspend(coroutineHandle);
            coroutine keeps suspended
            return to caller

        bool:
            bool result = awaitable.await_suspend(coroutineHandle);
            if result: 
                coroutine keep suspended
                return to caller
            else: 
                go to resumptionPoint

        another coroutine handle:	
            auto anotherCoroutineHandle = awaitable.await_suspend(coroutineHandle);
            anotherCoroutineHandle.resume();
            return to caller
	
resumptionPoint:

return awaitable.await_resume();

I presented the awaiter workflow in a pseudo-language. Understanding the awaiter workflow is the final puzzle piece for having an intuition about the behavior of coroutines and how you can adapt them.

What's next?

In my next post, I dig deeper into the awaiter workflow, which is based on the Awaitable. Be prepared for the double-edged sword. User-defined Awaitables give you great power but are challenging to understand.


Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dr?ge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang G?rtner, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschl?ger, Red Trip, Alexander Schwarz, and Tornike Porchxidze.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, and Richard Sargeant.

My special thanks to Embarcadero

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

New

Contact Me

Modernes C++

 

要查看或添加评论,请登录

社区洞察

其他会员也浏览了