A Generic Data Stream with Coroutines in C++20

In my last post in this mini-series on 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 explained 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.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    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 Compiler Explorer.
    coroutineGetElements
    To make it short. The implementation of  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 container elements. 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 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();
    }
    

    Following 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 based 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 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 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 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, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

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

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *