Concurrency[conc]
C++ generators: co_yield and std::generator
- Section
- Concurrency [conc]
- Standard
- C++23
- Compilers
- MSVC 19.44, Clang 19.1
- Published
- Length
- 7 min read
In this answer9 clauses
- 1The short answer: return std::generator and co_yield
- 2Lazy and single-pass
- 3A generator is not a thread
- 4Yielding another range: std::ranges::elements_of
- 5What makes a function a coroutine, and why it is stackless
- 6No
<generator>in your library? A C++20 generator - 7The lifetime trap: reference parameters and capturing lambdas
- 8Before C++20: macro-based and stackful coroutines
- 9Which standard libraries ship std::generator
Abstract
Return std::generator<T> from a function and co_yield each value: a range-for or a view pulls the values one at a time, and the body runs only during those pulls, on the caller's thread. std::generator is C++23 and ships in Microsoft's STL and libstdc++ but not yet in libc++, so in C++20 or on libc++ use a small hand-written generator, and in either case pass coroutine parameters by value.

C++’s answer to Python’s yield is a function that returns std::generator<T> and calls co_yield once per value. The caller iterates the result like any range, and the body runs only as far as the next value:
#include <generator>
#include <iostream>
#include <ranges>
#include <utility>
std::generator<long long> fibonacci() {
long long a = 0, b = 1;
while (true) {
co_yield a;
a = std::exchange(b, a + b);
}
}
int main() {
for (long long x : fibonacci() | std::views::take(10))
std::cout << x << ' '; // 0 1 1 2 3 5 8 13 21 34
}— end example]
This guide is written for C++23, the standard that added std::generator to the library (P2502R2). The language half, coroutines and co_yield, is C++20, but C++20 shipped no generator type, so C++20 code has to write one; a complete one is below. Before C++20 there were only library emulations. In the C++23 table of Since When, std::generator is listed for libstdc++ 14.1 and Visual Studio 2022 17.13, and as not yet in libc++; coroutines themselves are in its C++20 table. Every sample was compiled with MSVC 19.44 (/std:c++23preview /EHsc /W4 /permissive-) and Clang 19.1 (-std=c++23 -Wall -Wextra, on Microsoft’s standard library) on one Windows x64 machine, without optimization unless the text says otherwise, and the hand-written generator also under /std:c++20 and -std=c++20, unoptimized and at /O2 and -O2; the outputs shown are what those builds produced there, and what both printed.
What changed: a C++ generator is a coroutine type you write yourself, promise_type and allstd::generator<T> from <generator>.
since C++23
The short answer: return std::generator and co_yield
fibonacci is a coroutine because its body contains co_yield. Calling it runs none of the body: it returns a std:: that holds the suspended function. Each step of the loop resumes the body until the next co_yield, takes that value and suspends the body again. The while (true) is safe because std::views::take(10) stops asking after ten values, and the generator, with its suspended body, is destroyed when the loop ends.
<generator> needs C++23 mode: -std=c++23 for Clang and GCC, /std:c++latest or /std:c++23preview for MSVC. Under /std:c++20, Microsoft’s header warns (STL4038) and declares nothing.
The template argument decides what each step hands you:
| Declared as | *it gives |
Value type | Fits |
|---|---|---|---|
std::generator<T> |
T&& |
T |
values computed in the body |
std:: |
const T& |
T |
elements of data that outlives the loop |
std:: |
std::string_view |
std::string |
views whose stored copies must own their text |
co_yield x with a named x in a std::generator<T> yields a copy, since an lvalue can’t bind to T&&; a const T& generator hands out references with no copy.
Lazy and single-pass
#include <generator>
#include <iostream>
std::generator<int> numbers() {
std::cout << "start\n";
for (int i = 1; i <= 2; ++i) {
std::cout << "yield " << i << '\n';
co_yield i;
}
std::cout << "done\n";
}
int main() {
auto g = numbers(); // nothing printed: the body hasn't started
std::cout << "created\n";
for (int x : g)
std::cout << "got " << x << '\n';
}— end example]
Both compilers print created, start, yield 1, got 1, yield 2, got 2, done, in that order. The body starts at the loop’s first call to begin(), and each ++ runs it to the next co_yield.
A std::generator is a view and an input range, not a forward range, and it is move-only. Its begin() may be called once: the working draft makes “suspended at its initial suspend point” a precondition ([coro.generator.members]), so a second loop over the same generator is undefined behavior. Algorithms that need a forward range, such as std::, don’t compile on it. When you need two passes, store the values:
#include <algorithm>
#include <concepts>
#include <generator>
#include <iostream>
#include <ranges>
#include <vector>
std::generator<int> evens(int n) {
for (int i = 0; i < n; i += 2)
co_yield i;
}
static_assert(std::ranges::view<std::generator<int>>);
static_assert(std::ranges::input_range<std::generator<int>>);
static_assert(!std::ranges::forward_range<std::generator<int>>);
static_assert(!std::copyable<std::generator<int>>);
int main() {
// Two passes, or an algorithm that needs a forward range?
// Store the values first (std::ranges::to is C++23 too).
auto v = std::ranges::to<std::vector>(evens(10));
std::cout << *std::ranges::max_element(v) << ' ' << v.size() << '\n';
// prints: 8 5
}— end example]
A generator is not a thread
Nothing in a generator runs in the background. Its body runs on whatever thread resumes it, inside that thread’s call to begin() or ++, and the thread is back in the loop the moment the body reaches co_yield. A generator iterated inside a std::jthread runs on that thread; one iterated in main runs on the main thread (a check with std:: on both compilers here showed exactly that). Two generators in one loop take turns on one thread, and a generator touched by two threads needs a mutex like any other object. Coroutines that wait for other threads or for I/O are built on co_await, which std::generator leaves out: a co_await in its body fails to compile on both compilers here. As Microsoft’s announcement of its implementation puts it, “std::generator is synchronous”. The rest of Concurrency covers the asynchronous side.
Yielding another range: std::ranges::elements_of
co_yield std:: yields every element of r in turn. With a nested generator, it runs the inner generator directly instead of passing each value up through every level, so a recursive generator reads like the recursive function it replaces:
#include <generator>
#include <iostream>
#include <ranges>
#include <string>
#include <vector>
struct Node {
std::string name;
std::vector<Node> children;
};
// Depth-first, parent first. The tree must outlive the loop.
std::generator<const Node&> depth_first(const Node& n) {
co_yield n;
for (const Node& child : n.children)
co_yield std::ranges::elements_of(depth_first(child));
}
int main() {
Node root{"src", {{"core", {{"io", {}}, {"net", {}}}}, {"tools", {}}}};
for (const Node& n : depth_first(root))
std::cout << n.name << ' '; // src core io net tools
}— end example]
elements_of takes any range whose reference type converts to the generator’s yielded type, which for std::generator<int> is int&&. That rule catches people out:
#include <generator>
#include <iostream>
#include <ranges>
#include <vector>
// std::generator<int> hands out int&&, and an lvalue vector<int> gives
// int&, so elements_of(local) would not compile there. const int& does.
std::generator<const int&> all_ids(std::vector<int> local,
std::vector<int> remote) {
co_yield std::ranges::elements_of(local);
co_yield 0; // a separator
co_yield std::ranges::elements_of(remote);
}
int main() {
for (int id : all_ids({1, 2}, {7, 8, 9}))
std::cout << id << ' '; // 1 2 0 7 8 9
}— end example]
To keep std::generator<int>, move the elements out instead: co_yield std:: (C++23) compiles on both compilers and prints the same.
What makes a function a coroutine, and why it is stackless
A function is a coroutine if its body contains co_await, co_yield or co_return. Its return type then decides what the coroutine does, through the promise type that std:: finds for it, by default the return type’s nested promise_type. Coroutines can’t use a plain return, a deduced return type (auto or a concept), or C-style variadic arguments, and constructors, destructors, constexpr and consteval functions and main can’t be coroutines.
Calling a coroutine creates its frame, the coroutine state: the copies of its parameters, its promise, and the locals that live across a suspension. The frame is allocated with operator new, and the compiler may remove that allocation when the coroutine’s lifetime is nested inside the caller’s and the frame’s size is known, so count on neither.
C++20 coroutines are stackless: a coroutine suspends by returning to its caller, and only its own body can suspend. A function it calls cannot co_yield on its behalf, which is why the tree above makes every level its own generator and joins them with elements_of.
No <generator> in your library? A C++20 generator
This is the whole type, for C++20, for libc++, and for anyone who wants to see the machinery. It yields const T&, works with range-for and the standard views, and has no elements_of or allocator support:
// generator.hpp: a small C++20 generator for libraries without
// <generator>. Move-only, single-pass, yields const T&.
#include <coroutine>
#include <cstddef>
#include <exception>
#include <iterator>
#include <memory>
#include <ranges>
#include <utility>
template <class T>
class generator : public std::ranges::view_interface<generator<T>> {
public:
struct promise_type {
const T* current = nullptr; // the value at the last co_yield
std::exception_ptr error; // what escaped the body, if anything
generator get_return_object() {
return generator{handle::from_promise(*this)};
}
// Lazy: the body starts at the first begin().
std::suspend_always initial_suspend() noexcept { return {}; }
// Stop at the end too, so that ~generator destroys the frame.
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(const T& value) noexcept {
current = std::addressof(value); // valid until resumed
return {};
}
void return_void() noexcept {}
void unhandled_exception() { error = std::current_exception(); }
template <class U> // no co_await in a generator
std::suspend_never await_transform(U&&) = delete;
};
using handle = std::coroutine_handle<promise_type>;
class iterator {
public:
using iterator_concept = std::input_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
iterator() = default;
explicit iterator(handle h) : h_(h) {}
const T& operator*() const { return *h_.promise().current; }
iterator& operator++() {
resume(h_);
return *this;
}
void operator++(int) { ++*this; }
bool operator==(std::default_sentinel_t) const {
return h_.done();
}
private:
handle h_ = nullptr; // not owned
};
generator(generator&& other) noexcept
: h_(std::exchange(other.h_, nullptr)) {}
generator& operator=(generator other) noexcept {
std::swap(h_, other.h_);
return *this;
}
~generator() {
if (h_) h_.destroy(); // also when suspended mid-sequence
}
iterator begin() { // once only: the sequence is single-pass
resume(h_);
return iterator{h_};
}
std::default_sentinel_t end() const noexcept { return {}; }
private:
explicit generator(handle h) : h_(h) {}
// Run the body to its next co_yield or to its end, then rethrow
// whatever escaped it.
static void resume(handle h) {
h.resume();
if (auto e = std::exchange(h.promise().error, nullptr))
std::rethrow_exception(e);
}
handle h_ = nullptr; // owned: one frame, destroyed with *this
};— end example]
And a program that shows its lifetime rules:
#include "generator.hpp"
#include <iostream>
#include <ranges>
#include <stdexcept>
struct trace {
const char* what;
~trace() { std::cout << " " << what << " destroyed\n"; }
};
generator<int> squares(int limit) {
trace t{"squares frame"};
for (int i = 1; i <= limit; ++i)
co_yield i * i;
}
generator<int> broken() {
trace t{"broken frame"};
co_yield 1;
throw std::runtime_error("input closed");
}
int main() {
for (int x : squares(1000)) {
if (x > 10) break; // ~generator destroys the suspended frame
std::cout << x << '\n';
}
for (int x : squares(1000) | std::views::drop(2)
| std::views::take(2))
std::cout << x << '\n';
try {
for (int x : broken())
std::cout << x << '\n';
} catch (const std::exception& e) {
std::cout << "caught: " << e.what() << '\n';
}
}— end example]
Both compilers print 1 4 9, then squares frame destroyed, then 9 16 and squares frame destroyed again, then 1, broken frame destroyed and caught: input closed (one item per line). The rules behind that output:
- The generator object owns the frame. The
coroutine_handleinside it is a non-owning handle; the class is move-only, likestd::unique_ptr, and its destructor callsdestroy(). A moved-from generator holds a null handle and must not be iterated. - Nothing runs at the call.
squares(1000)allocates the frame, copieslimitinto it, creates the promise and stops atinitial_suspend, before the first statement. - Early destruction is clean. Leaving the loop destroys the generator while its body is suspended at a
co_yield. Destroying a suspended coroutine runs the destructors of the locals in scope at that point and of the parameter copies, then frees the frame: that is the firstsquares frame destroyed. - Iterators and values borrow. An iterator doesn’t own the frame, and
*itrefers to the value at the lastco_yield, valid until the next++. Even the temporary inco_yield i * ilives that long, because the coroutine suspends inside that full-expression. Copy the value if you need it longer. - Exceptions reach the caller. An exception that leaves the body goes to
unhandled_exception, which stores it;resumerethrows it frombegin()or++. The coroutine is then at its final suspend point, so the generator destroys it as usual. The standard also allowsthrow;insideunhandled_exception, which is whatstd::generatordoes for a generator that isn’t nested. With that version of this class, Clang 19 at-O2on Windows ran a local’s destructor twice on this path here (Clang’s status page lists coroutines as partial, with stability issues on Windows), so this one stores the exception and rethrows it afterresume()returns. - No
co_await. The deletedawait_transformturns aco_awaitin a generator’s body into a compile error on both compilers, as it is instd::generator.
The lifetime trap: reference parameters and capturing lambdas
A coroutine’s frame keeps copies of its parameters, but a copy of a reference is still a reference. And since a generator’s body doesn’t start until the first begin(), anything a reference parameter refers to has to outlive the whole iteration, not only the call:
#include <generator>
#include <iostream>
#include <string>
// Bad: a reference parameter (Core Guidelines CP.53).
std::generator<char> letters(const std::string& s) {
for (char c : s)
co_yield c;
}
int main() {
auto g = letters("temporary"); // the std::string dies at the ';'
for (char c : g) // undefined behavior: reads it
std::cout << c;
}— end example]
The Core Guidelines’ rule CP.53 says “Parameters to coroutines should not be passed by reference”, and its note singles out coroutines that suspend before their first line runs, as std::generator and the type above do: for those it calls reference parameters always unsafe. Take the parameter by value, std::, and the frame owns its copy; C++ pass by value or const reference covers that choice outside coroutines. A loop header is the one place where the reference version happens to work in C++23, which keeps the temporaries of a range-for initializer alive until the loop ends (P2718R0). Don’t lean on it: the rule covers nothing outside the loop header, and not every compiler applies it yet. Here, Clang 19 reported __cpp_range_based_for as 202211L under -std=c++23, while MSVC 19.44 reported 201603L, the C++17 value. The tree walk above takes const Node& anyway, and is correct only because root outlives the loop: exactly the reasoning CP.53 asks you not to depend on.
Capturing lambdas fail the same way, from the other side (CP.51). A lambda’s captures live in the closure object, not in the coroutine frame, and the body reaches them through the closure:
#include <generator>
#include <iostream>
#include <string>
int main() {
// Bad: a capturing lambda that is a coroutine (CP.51).
auto g = [p = std::string("id-")]() -> std::generator<std::string> {
for (int i = 0; i < 3; ++i)
co_yield p + std::to_string(i);
}(); // the closure, and p with it, dies at the ';'
for (const std::string& id : g) // undefined behavior
std::cout << id << ' ';
}— end example]
Pass what the lambda needs as a parameter instead, and the frame owns it:
// Inside main, with the same includes:
auto make = [](std::string prefix) -> std::generator<std::string> {
for (int i = 0; i < 3; ++i)
co_yield prefix + std::to_string(i);
};
for (const std::string& id : make("id-"))
std::cout << id << ' '; // id-0 id-1 id-2— end example]
C++ generic lambdas, template lambdas and function objects shows what a closure object holds. Both bad samples compile without a warning at /W4 and -Wall -Wextra. clang-tidy, on Toolchain, has a check for each rule: cppcoreguidelines-avoid-reference-coroutine-parameters and cppcoreguidelines-avoid-capturing-lambda-coroutines.
One more trap is in the reference type. A std:: hands out views that may point into the frame, and its value type is std::string_view too, so std:: would store views of a buffer that the body overwrites and then frees. Name an owning value type as the second argument:
#include <generator>
#include <iostream>
#include <ranges>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
// Reference type std::string_view, value type std::string.
std::generator<std::string_view, std::string> words(std::string text) {
std::string word;
for (char c : text + ' ') {
if (c != ' ') { word += c; continue; }
if (!word.empty()) co_yield word;
word.clear();
}
}
int main() {
auto v = std::ranges::to<std::vector>(words("one two three"));
static_assert(std::is_same_v<decltype(v), std::vector<std::string>>);
for (const auto& w : v) std::cout << w << '|'; // one|two|three|
}— end example]
Before C++20: macro-based and stackful coroutines
Before the language had coroutines, libraries emulated them in two ways. Boost.Asio’s asio::coroutine is stackless: reenter, yield and fork are macros, reenter is implemented with a switch, and the whole state is a single int. Because re-entering jumps into the middle of the function, a local variable can’t sit where the jump would bypass its definition, so data that must survive a yield lives outside the function, typically in the members of the object that holds the coroutine. Boost.Coroutine2 is stackful: each coroutine has its own stack, switched with Boost.Context, and can suspend from inside a nested call, which C++20 coroutines can’t. C++20 made the stackless kind a language feature; stackful coroutines are still library-only.
Which standard libraries ship std::generator
std::generator is a library feature, so what matters is the standard library your compiler builds against:
| Standard library | Used by | std::generator |
|---|---|---|
| Microsoft STL | MSVC, and Clang targeting MSVC (both tested here) | Visual Studio 2022 17.13 |
| libstdc++ | GCC, and Clang built against it | GCC 14.1 |
| libc++ | Clang on macOS and FreeBSD, Apple’s Clang | Not yet |
The libstdc++ cell comes from the GCC 14 release notes, which list “The std::generator view for getting results from coroutines”; there is no GCC on this machine, so it is cited, not tested. libc++’s C++23 status page gives P2502R2 no status and points to an open tracking issue. On macOS, use the C++20 type above. Clang’s own status page lists coroutines as partial, with stability and ABI issues on Windows; every sample here that is meant to run ran correctly on Clang 19 for x64 Windows, and the one misbehavior seen was the variant described in rule 5.
To use std::generator where it exists and the hand-written type elsewhere, test the feature macro, which is 202207L on both compilers here in C++23 mode:
#include <version>
#if defined(__cpp_lib_generator) && __cpp_lib_generator >= 202207L
#include <generator>
template <class T>
using gen = std::generator<T>;
#else
#include "generator.hpp" // the C++20 type from this article
template <class T>
using gen = generator<T>;
#endif— end example]
Code written against gen<T> that only loops over the values compiles either way; the two differ in what *it gives (T&& against const T&) and in elements_of, which only std::generator has.
Questions
Does C++ have yield like Python?
Yes, since C++20: co_yield inside a function makes it a coroutine that hands out one value per step. The function must return a generator type; C++23 provides std::generator in <generator>, and C++20 code writes its own. Like a Python generator, a std::generator is lazy and can be iterated once.
Does a C++ generator run on another thread?
No. The body runs on whichever thread resumes it, which for a range-for is the thread running the loop, and it runs only inside begin() and ++. Nothing is computed in the background, and a generator shared between threads needs the same locking as any other object.
Can I iterate a std::generator twice?
No. It is a single-pass input range, and calling begin() a second time breaks its precondition, which is undefined behavior. Call the function again for a fresh sequence, or store the values first with std::.
Which compilers support std::generator?
It depends on the standard library: Microsoft’s STL has it since Visual Studio 2022 17.13 and libstdc++ since GCC 14.1, while libc++ lists no status for it yet. Clang 19 compiled every sample here against Microsoft’s STL with -std=c++23; MSVC needs /std:c++latest or /std:c++23preview.
Why are C++20 coroutines called stackless?
Because a coroutine suspends by returning to its caller and keeps its state in a separately allocated frame, not on a stack of its own. Only the coroutine’s own body can suspend; a function it calls cannot suspend on its behalf. Stackful coroutines, such as Boost.Coroutine2, give each coroutine its own stack instead.
Sources
- std::generatorcppreference.com
- Coroutines (C++20)cppreference.com
- std::generator: Standard Library Coroutine SupportMicrosoft C++ Team Blog,
- C++ Core Guidelines, CP.coro: CoroutinesStandard C++ Foundation