Generators
A generator is a coroutine whose promise stores one value and suspends after each one. The caller asks for the next value, resumes the coroutine, and gets the value it produced on the way to the next suspension.
The machinery is the coroutine of lesson 115: co_yield value is co_await on the promise's yield_value, which stores the value and suspends. What this lesson adds is the iterator that makes the sequence usable in a range-for, and the laziness that comes with it.
A generator that owns a resource should release it when the loop ends, and the destructor of the frame is where that happens. A coroutine that is never resumed never runs its body, so a resource acquired before the first yield would be held by a sequence nobody used.
The iterator only needs the operations a range-for uses, and the standard input iterator concept asks for a little more. std::generator, added in C++23, is the version to use when the type has to satisfy it.
co_yield and the iterator
co_yield value stores the value in the promise and suspends. The generator's iterator reads the promise with operator* and resumes the coroutine in operator++, so a range-for walks the sequence one resume at a time.
The program's coroutine yields 1, 2 and 3 and then ends. The range-for prints them with the spaces the loop writes, and the iterator compares equal to end() when the handle is gone.
The generator owns the handle and destroys it in its destructor, which is why it is move-only: two objects with one handle would destroy the frame twice. The range-for keeps the temporary alive for the length of the loop.
#include <coroutine>
#include <exception>
#include <iostream>
class generator
{
public:
struct promise_type
{
int current = 0;
generator get_return_object()
{
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(int value)
{
current = value;
return {};
}
};
explicit generator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
~generator()
{
if (handle_) {
handle_.destroy();
}
}
class iterator
{
public:
explicit iterator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
int operator*() const
{
return handle_.promise().current;
}
iterator& operator++()
{
handle_.resume();
if (handle_.done()) {
handle_ = nullptr;
}
return *this;
}
bool operator==(const iterator& other) const
{
return handle_ == other.handle_;
}
private:
std::coroutine_handle<promise_type> handle_;
};
iterator begin()
{
if (!handle_) {
return iterator{nullptr};
}
handle_.resume();
if (handle_.done()) {
return iterator{nullptr};
}
return iterator{handle_};
}
iterator end()
{
return iterator{nullptr};
}
private:
std::coroutine_handle<promise_type> handle_;
};
generator numbers()
{
co_yield 1;
co_yield 2;
co_yield 3;
}
int main()
{
for (int value : numbers()) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra numbers.cpp -o numbers
./numbers
1 2 3
Nothing runs until asked
The coroutine starts suspended, and begin is what resumes it for the first value. The program prints a marker before the generator is created, one after, and the coroutine prints its own markers around each yield.
The output is before, created, start, 1, middle, 2, end: the body runs only when the loop asks, and each request runs the coroutine up to the next yield. That is what makes a generator cheap for a long or infinite sequence.
The same laziness is what lets a generator hold a resource: a file opened before the first yield is opened when the loop starts, and a loop that breaks early never runs the rest of the body.
#include <coroutine>
#include <exception>
#include <iostream>
class generator
{
public:
struct promise_type
{
int current = 0;
generator get_return_object()
{
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(int value)
{
current = value;
return {};
}
};
explicit generator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
~generator()
{
if (handle_) {
handle_.destroy();
}
}
class iterator
{
public:
explicit iterator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
int operator*() const
{
return handle_.promise().current;
}
iterator& operator++()
{
handle_.resume();
if (handle_.done()) {
handle_ = nullptr;
}
return *this;
}
bool operator==(const iterator& other) const
{
return handle_ == other.handle_;
}
private:
std::coroutine_handle<promise_type> handle_;
};
iterator begin()
{
if (!handle_) {
return iterator{nullptr};
}
handle_.resume();
if (handle_.done()) {
return iterator{nullptr};
}
return iterator{handle_};
}
iterator end()
{
return iterator{nullptr};
}
private:
std::coroutine_handle<promise_type> handle_;
};
generator count()
{
std::cout << "start ";
co_yield 1;
std::cout << "middle ";
co_yield 2;
std::cout << "end";
}
int main()
{
std::cout << "before ";
generator values = count();
std::cout << "created ";
for (int value : values) {
std::cout << value << ' ';
}
std::cout << '\n';
}
g++ -std=c++23 -Wall -Wextra lazy.cpp -o lazy
./lazy
before created start 1 middle 2 end
A sequence that never ends
A generator does not have to know where it stops: the loop decides. The program's coroutine yields the Fibonacci numbers forever, and main takes the first five and breaks.
The coroutine keeps a and b as locals in its frame, so the state survives from one resume to the next without a class or a static. The printed sequence is 0, 1, 1, 2, 3 and the sum is 7.
Breaking out of the range-for destroys the generator, whose destructor destroys the frame. The locals are destroyed with it, which is the RAII guarantee of lesson 095 reaching a coroutine.
#include <coroutine>
#include <exception>
#include <iostream>
class generator
{
public:
struct promise_type
{
int current = 0;
generator get_return_object()
{
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(int value)
{
current = value;
return {};
}
};
explicit generator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
~generator()
{
if (handle_) {
handle_.destroy();
}
}
class iterator
{
public:
explicit iterator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
int operator*() const
{
return handle_.promise().current;
}
iterator& operator++()
{
handle_.resume();
if (handle_.done()) {
handle_ = nullptr;
}
return *this;
}
bool operator==(const iterator& other) const
{
return handle_ == other.handle_;
}
private:
std::coroutine_handle<promise_type> handle_;
};
iterator begin()
{
if (!handle_) {
return iterator{nullptr};
}
handle_.resume();
if (handle_.done()) {
return iterator{nullptr};
}
return iterator{handle_};
}
iterator end()
{
return iterator{nullptr};
}
private:
std::coroutine_handle<promise_type> handle_;
};
generator fibonacci()
{
int a = 0;
int b = 1;
while (true) {
co_yield a;
int next = a + b;
a = b;
b = next;
}
}
int main()
{
int sum = 0;
int taken = 0;
for (int value : fibonacci()) {
std::cout << value << ' ';
sum += value;
if (++taken == 5) {
break;
}
}
std::cout << "sum " << sum << '\n';
}
g++ -std=c++23 -Wall -Wextra fibonacci.cpp -o fibonacci
./fibonacci
0 1 1 2 3 sum 7
A generator is not copyable
The generator owns the frame through its handle, and a copy would give two destructors the same handle. The copy constructor is deleted, so a function that takes the generator by value fails to compile.
The program copies a generator and the compiler reports the deleted copy constructor. The message names the class and the function, and it is the same error as the unique_ptr of lesson 091.
Pass a generator by reference, or move it when the loop is the last user. A generator that has been moved from has a null handle, and its begin returns the end iterator, which is why the moved-from state is safe.
#include <coroutine>
#include <exception>
#include <iostream>
class generator
{
public:
struct promise_type
{
int current = 0;
generator get_return_object()
{
return generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(int value)
{
current = value;
return {};
}
};
explicit generator(std::coroutine_handle<promise_type> handle) : handle_(handle) {}
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
~generator()
{
if (handle_) {
handle_.destroy();
}
}
std::coroutine_handle<promise_type> handle_;
};
generator numbers()
{
co_yield 1;
}
int main()
{
generator first = numbers();
generator second = first;
}
g++ -std=c++23 -Wall -Wextra copy.cpp -o copy
copy.cpp: In function 'int main()':
copy.cpp:52:24: error: use of deleted function 'generator::generator(const generator&)'
52 | generator second = first;
| ^~~~~
copy.cpp:31:5: note: declared here
31 | generator(const generator&) = delete;
| ^~~~~~~~~
copy.cpp:52:24: note: use '-fdiagnostics-all-candidates' to display considered candidates
52 | generator second = first;
| ^~~~~
Exercises
- Write a generator that yields the squares of 1 to 10 and print them in a range-for.
- Add a `take(int n)` function that wraps a generator and stops after n values.
- Make the Fibonacci generator yield long long and check that the tenth value is still exact.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
co_yield valuedo?Answer
It stores the value in the promise and suspends the coroutine
The compiler calls yield_value on the promise, which stores the value, and then suspends.
-
What does the iterator's
operator++do?Answer
It resumes the coroutine and marks the end when the coroutine is done
One resume runs the body up to the next yield or to its end, which is one step of the sequence.
-
When does the body of a generator start running?
Answer
At the first call to begin, which is the first resume
initial_suspend returns suspend_always, so the body waits for the first request.
-
What does this program print?
#include <coroutine> #include <iostream> struct gen { struct promise_type { int value = 0; gen get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} std::suspend_always yield_value(int v) { value = v; return {}; } }; std::coroutine_handle<promise_type> h; bool next() { if (!h || h.done()) return false; h.resume(); return !h.done(); } int current() const { return h.promise().value; } ~gen() { if (h) h.destroy(); } }; gen numbers() { co_yield 1; co_yield 2; co_yield 3; } int main() { gen g = numbers(); while (g.next()) std::cout << g.current() << ' '; }Answer
1 2 3
The coroutine yields exactly the three values, and the loop stops when it ends.
-
What does this program print?
#include <coroutine> #include <iostream> struct gen { struct promise_type { int value = 0; gen get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} std::suspend_always yield_value(int v) { value = v; return {}; } }; std::coroutine_handle<promise_type> h; bool next() { if (!h || h.done()) return false; h.resume(); return !h.done(); } int current() const { return h.promise().value; } ~gen() { if (h) h.destroy(); } }; gen count() { std::cout << "start "; co_yield 1; std::cout << "middle "; co_yield 2; std::cout << "end"; } int main() { std::cout << "before "; gen g = count(); std::cout << "created "; while (g.next()) std::cout << g.current() << ' '; }Answer
before created start 1 middle 2 end
The markers in main run first, and the coroutine's lines appear only as the loop resumes it.
-
What does the Fibonacci program print?
Answer
0 1 1 2 3 sum 7
The first five values are 0, 1, 1, 2 and 3, and their sum is 7.
-
Why is the generator move-only?
Answer
Because it owns the coroutine frame through one handle
Two owners of one handle would destroy the same frame twice.
-
What does
final_suspendreturningsuspend_alwaysmean for the frame?Answer
The frame stays alive after the coroutine ends, until the handle destroys it
The final suspend is the last pause, and the owner of the handle is responsible for the destruction.
-
What does
handle.done()report?Answer
That the coroutine has reached its end and is suspended there
done is true after the body ran to completion and the final suspend suspended it.
-
How many values does the Fibonacci generator produce?
Answer
As many as the loop asks for, because the coroutine never ends on its own
The while loop in the coroutine has no end condition, and the caller's break decides where the sequence stops.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev