Case Study III: Modification and Generalization of a Generator
Improve the generator from the section discussing the 'co_return' keyword.
We'll cover the following...
Before I modify and generalize the generator for an infinite data stream, I want to present it as a starting point of our journey. I intentionally put many output operations in the source code and only ask for three values. These simplifications and visualizations should help to understand the control flow.
#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();return coro.promise().current_value;}struct promise_type {promise_type() {std::cout << " promise_type::promise_type" << '\n';}~promise_type() {std::cout << " promise_type::~promise_type" << '\n';}std::suspend_always initial_suspend() {std::cout << " promise_type::initial_suspend" << '\n';return {};}std::suspend_always final_suspend() noexcept {std::cout << " promise_type::final_suspend" << '\n';return {};}auto get_return_object() {std::cout << " promise_type::get_return_object" << '\n';return Generator{handle_type::from_promise(*this)};}std::suspend_always yield_value(int value) {std::cout << " promise_type::yield_value" << '\n';current_value = value;return {};}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 (int i = 0;; ++i){std::cout << " getNext: before co_yield" << '\n';co_yield value;std::cout << " getNext: after co_yield" << '\n';value += step;}}int main() {auto gen = getNext();for (int i = 0; i <= 2; ++i) {auto val = gen.getNextValue();std::cout << "main: " << val << '\n';}}
Let’s analyze the control flow.
The call getNext()
(line 84) triggers the creation of the Generator<int>
. First, the promise_type
(line 36) is created, and the following get_return_object
call (line 53) creates the generator (line 55) 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 46). Because the member function call initial_suspend
returns an awaitable std::suspend_always
(line 45), the control flow continues with the coroutine getNext
until the instruction co_yield value
(line 77). This call is mapped to the call yield_value(int value)
(line 58) and the current value is prepared current_value = value
(line 60 ...