An Infinite Data Stream with Coroutines in C++20

Contents[Show]

In this post, I analyze the new keyword co_yield. Thanks to co_yield, you can create an infinite data stream in C++20.

 

This is what happened so far in my pragmatical journey through the new coroutine keywords co_return, co_yield, and co_await.

co_return:

A Generator

As a starting point for further variations, I want to present to start with a generator which I only ask for three values. This simplification and visualization should help to understand the control flow of the generator.

 

// infiniteDataStreamComments.cpp

#include <coroutine>
#include <memory>
#include <iostream>

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

    handle_type coro;
    
    ~Generator() {
        std::cout << "        Generator::~Generator" << '\n';
        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() {
        std::cout << "        Generator::getNextValue" << '\n';
        coro.resume();                                                  // (13) 
        return coro.promise().current_value;
    }
    struct promise_type {
        promise_type() {                                                // (2)
            std::cout << "            promise_type::promise_type" << '\n';
        }                              
          
        ~promise_type() {
            std::cout << "            promise_type::~promise_type" << '\n';
        }
        
        std::suspend_always initial_suspend() {                         // (5)
            std::cout << "            promise_type::initial_suspend" << '\n';  
            return {};                                                  // (6)
        }
        std::suspend_always final_suspend() noexcept {
            std::cout << "            promise_type::final_suspend" << '\n';
            return {};
        }
        auto get_return_object() {                                       // (3)
            std::cout << "            promise_type::get_return_object" << '\n';
            return Generator{handle_type::from_promise(*this)};          // (4)
        }
      
        std::suspend_always yield_value(int value) {                     // (8)
            std::cout << "            promise_type::yield_value" << '\n';
            current_value = value;                                       // (9)
            return {};                                                   // (10)
        }
        void return_void() {}
        void unhandled_exception() {
            std::exit(1);
        }

        T current_value;
    };

};

Generator<int> getNext(int start = 10, int step = 10) {
    std::cout << "    getNext: start" << '\n';
    auto value = start;
    for (true) {                                                         // (11)
        std::cout << "    getNext: before co_yield" << '\n';
        co_yield value;                                                  // (7)
        std::cout << "    getNext: after co_yield" << '\n';
        value += step;
    }
}

int main() {
  
    auto gen = getNext();                                                // (1)
    for (int i = 0; i <= 2; ++i) {
        auto val = gen.getNextValue();                                   // (12)
        std::cout << "main: " << val << '\n';                            // (14)
    }
    
}

 

Executing the program on the Compiler Explorer makes the control flow transparent.
 
infiniteDataStreamComments

 

Let's analyze the control flow.

The call getNext() (line 1) triggers the creation of the Generator<int>. First, promise_type (line 2) is created, and the following get_return_object call (line 3) creates the generator (line 4) and stores it in a local variable. The result of this call is returned to the caller when the coroutine is suspended the first time. The initial suspension happens immediately (line 5). Because the member function call initial_suspend returns an Awaitable std::suspend_always (line 6), the control flow continues with the coroutine getNext until the instruction co_yield value (line 7). This call is mapped to the call yield_value(int value) (line 8) and the current value is prepared current_value = value (line 9). The member function yield_value(int value) returns the Awaitable std::suspend_always (line 10). Consequently, the execution of the coroutine pauses, the control flow goes back to the main function, and the for loop starts (line 11). The call gen.getNextValue() (line 12) starts the execution of the coroutine by resuming the coroutine, using coro.resume() (line 13). Further, the function getNextValue() returns the current value that was prepared using the previously invoked member function yield_value(int value) (line 8). Finally, the generated number is displayed in line14 and the for loop continues. In the end, the generator and the promise are destructed.

After this detailed analysis, I want to make a first modification of the control flow.

Modifications

My code snippets and line numbers are all based on the previous program infiniteDataStreamComments.cpp. I only show the modifications.
 

The Coroutine is Not Resumed

When I disable the resumption of the coroutine (gen.getNextValue() in line 12) and the display of its value (line 14), the coroutine immediately pauses. 
 
int main() {
  
    auto gen = getNext();
    for (int i = 0; i <= 2; ++i) {
        // auto val = gen.getNextValue();
        // std::cout << "main: " << val << '\n';                   
    }
    
}
 

The coroutine never runs. Consequently, the generator and its promise are created and destroyed. 
 
infiniteDataStreamCoroutineNeverRuns
 

initial_suspend Never Suspends

In the program, the member function initial_suspend returns the Awaitable std::suspend_always (line 5). As its name suggests, the Awaitable std::suspends_always causes the coroutine to pause immediately. Let me return std::suspend_never instead of std::suspend_always.

std::suspend_never initial_suspend() {  
    std::cout << "            promise_type::initial_suspend" << '\n';
    return {};
}
In this case, the coroutine runs immediately and pauses when the function yield_value (line 8) is invoked. A subsequent call gen.getNextValue() (line 12) resumes the coroutine and triggers the execution of the member function yield_value once more. The result is that the start value 10 is ignored, and the coroutine returns the values 20, 30, and 40.
 
infiniteDataStreamInitialSuspendSuspendsNever
 

yield_value Never Suspends

 
The member function yield_value (line 8) is triggered by the call co_yield value and prepares the current_value (line 9). The function returns the Awaitable std::suspend_always (line 10) and, therefore, pauses the coroutine. Consequently, a subsequent call gen.getNextValue (line 12) has to resume the coroutine. When I change the return value of the member function yield_value to std::suspend_never, let me see what happens.
 
 
std::suspend_never yield_value(int value) {    
    std::cout << "            promise_type::yield_value" << '\n';
    current_value = value;
    return {};
}
 
As you may guess, the while loop (line 1) runs forever, and the coroutine does not return anything.
 
infiniteDataStreamCommentsYieldValueSuspendsNever
 
@Matt Godbolt: this was not a denial-of-service attack.

What's next?

So far, I never used the fact that the coroutine is a class template. In my next post, I restructure the generator so that it produces a finite number of arbitrary values.

 

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, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, 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, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Dimitrov Tsvetomir, Leo Goodstadt, Eduardo Velasquez, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, and Michael Young.

 

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

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

Seminars

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

Bookable (Online)

German

Standard Seminars (English/German)

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

New

Contact Me

Modernes C++,

RainerGrimmSmall

 
 
 

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 6494

Yesterday 7646

Week 39789

Month 106455

All 7374295

Currently are 162 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments