библиотека — coroutineslibrary — coroutines

std::generator

Готовый, стандартный тип-корутина для генерации последовательности значений через co_yield — закрывает главный пробел, оставленный coroutines в C++20: механизм был, готового типа не было.A ready-made, standard coroutine type for generating a sequence of values via co_yield — closes the main gap left by C++20 coroutines: the mechanism existed, a ready-made type did not.

закрывает известный пробел C++20closes a known C++20 gap ленивый, однопроходный, обычно не бесплатныйlazy, single-pass, usually not free частый вопрос: чем отличается от eager-контейнераcommon question: how it differs from an eager container

Что этоWhat it is

std::generator<T> (заголовок <generator>) — готовый тип-корутина: функция с возвращаемым типом std::generator<T>, использующая co_yield для отдачи значений по одному, автоматически становится ленивым генератором последовательности T, итерируемым через range-for или ranges-адаптеры. Это именно то, чего не хватало в C++20 — там coroutines были только низкоуровневым механизмом (promise_type, awaiter), а готовые высокоуровневые типы вроде Generator/Task приходилось писать самому или брать из сторонней библиотеки (см. подробный разбор этой проблемы в cpp20/coroutines.html).std::generator<T> (header <generator>) is a ready-made coroutine type: a function with return type std::generator<T> that uses co_yield to hand out values one at a time automatically becomes a lazy generator of a T sequence, iterable via range-for or ranges adaptors. This is exactly what was missing in C++20 — there, coroutines were only a low-level mechanism (promise_type, awaiter), and ready-made high-level types like Generator/Task had to be hand-written or pulled from a third-party library (see the detailed discussion of this problem in cpp20/coroutines.html).

C++20 — механизм есть, готового типа нетC++20 — the mechanism exists, no ready-made type
// как показано в cpp20/coroutines.html — Generator<T> условен,// as shown in cpp20/coroutines.html — Generator<T> here is illustrative,
// стандарт C++20 такого типа не даёт вообще// C++20 itself provides no such type at all
Generator<int> range(int n) {
    for (int i = 0; i < n; ++i) co_yield i;
}
// нужно самому написать promise_type, итератор, деструктор coroutine_handle —// you must hand-write promise_type, an iterator, the coroutine_handle destructor —
// десятки строк boilerplate до первой содержательной корутины// dozens of lines of boilerplate before the first meaningful coroutine
C++23 — std::generator, готовый тип из коробкиC++23 — std::generator, ready-made out of the box
#include <generator>

std::generator<int> range(int n) {
    for (int i = 0; i < n; ++i) co_yield i;
}
// ноль своего boilerplate — promise_type и итератор уже реализованы// zero boilerplate of your own — promise_type and the iterator are already there

Зачем ввелиWhy it was introduced

C++20 оставил ровно ту проблему, которую std::generator и решает:C++20 left exactly the problem that std::generator solves:

01

Каждый проект писал свой Generator<T> с нуляEvery project hand-wrote its own Generator<T> from scratch

Как прямо отмечено в cpp20/coroutines.html, голый механизм C++20 (promise_type + awaiter) требует реализовать промежуточный слой самостоятельно — обычно несколько десятков строк шаблонного кода на тип корутины, прежде чем можно написать хотя бы одну содержательную корутину-генератор. Каждый проект, желающий использовать coroutines для генерации значений, либо писал этот код заново, либо тянул стороннюю библиотеку.As explicitly noted in cpp20/coroutines.html, the bare C++20 mechanism (promise_type + awaiter) requires implementing that intermediate layer yourself — typically several dozen lines of boilerplate per coroutine type, before you can write even one meaningful generator coroutine. Every project wanting to use coroutines for value generation either wrote this code anew or pulled in a third-party library.

02

cppcoro и подобные библиотеки — не часть стандарта, разный код в разных проектахcppcoro and similar libraries weren't part of the standard — different code in different projects

cppcoro (упомянутая в cpp20/coroutines.html как типичное решение) закрывала пробел, но оставалась сторонней зависимостью — со своими требованиями к сборке, версионированием, риском несовместимости между проектами, использующими разные версии или разные такие библиотеки вовсе.cppcoro (mentioned in cpp20/coroutines.html as the typical fix) closed the gap, but remained a third-party dependency — with its own build requirements, versioning, and a risk of incompatibility between projects using different versions, or different such libraries altogether.

03

Стандартизация убирает необходимость писать этот код заново — как в своё время optionalStandardization removes the need to rewrite this code — much like optional once did

std::generator стандартизирует именно этот распространённый частный случай (не Task, не полноценный async — просто ленивый генератор значений) — по аналогии с тем, как std::optional избавил от необходимости писать свой "может не быть значения" тип в каждом проекте. Полноценные асинхронные Task-абстракции по-прежнему остаются за пределами стандартной библиотеки — std::generator закрывает именно generator-часть пробела, не весь пробел целиком.std::generator standardizes exactly this common special case (not Task, not full-blown async — just a lazy value generator) — analogous to how std::optional removed the need to write your own "may have no value" type in every project. Full-blown async Task abstractions still remain outside the standard library — std::generator closes specifically the generator part of the gap, not the whole gap.

Как это работает под капотомHow it works under the hood

Функция, возвращающая std::generator<T> и использующая co_yield, компилятором трансформируется в конечный автомат ровно по тому же протоколу, что описан в cpp20/coroutines.html (promise_type + awaiter) — разница в том, что весь этот протокол уже реализован внутри стандартной библиотеки для generator-случая, а не программистом вручную. Вызов такой функции не выполняет тело сразу — он создаёт объект std::generator<T>, приостановленный в начале, который начинает реально исполняться только при первом обращении (итерации).A function returning std::generator<T> and using co_yield is transformed by the compiler into a state machine following exactly the protocol described in cpp20/coroutines.html (promise_type + awaiter) — the difference is that the entire protocol is already implemented inside the standard library for the generator case, rather than by hand. Calling such a function does not run the body right away — it creates a std::generator<T> object, suspended at the start, that only begins actually executing on the first access (iteration).

#include <generator>
#include <iostream>

std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;              // отдать a наружу, приостановиться// hand a out, suspend
        auto next = a + b;
        a = b;
        b = next;
    }
}

for (int x : fibonacci() | std::views::take(10)) {  // компонуется с ranges напрямую// composes directly with ranges
    std::cout << x << ' ';
}
// бесконечный генератор, но вычисляется ровно 10 значений — лениво, по запросу// an infinite generator, but exactly 10 values get computed — lazily, on demand

Использование через range-for или ranges-адаптеры (см. cpp20/ranges-basics.html) работает потому, что std::generator<T> сам удовлетворяет концепту input_range — итератор генератора при разыменовании вызывает await_resume внутреннего awaiter-а, которое возобновляет корутину до следующего co_yield (или до co_return/конца функции, что означает конец диапазона).Using it via range-for or ranges adaptors (see cpp20/ranges-basics.html) works because std::generator<T> itself satisfies the input_range concept — dereferencing the generator's iterator calls the inner awaiter's await_resume, which resumes the coroutine up to the next co_yield (or up to co_return/the end of the function, which marks the end of the range).

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Ленивый обход дерева без промежуточного вектораA lazy tree traversal with no intermediate vector

Обход дерева в глубину, отдающий значения по одному через рекурсивный co_yield — без сбора всех узлов в vector заранее, что особенно важно для больших или потенциально бесконечных структур.A depth-first tree traversal handing out values one at a time via a recursive co_yield — with no need to collect all nodes into a vector up front, especially useful for large or potentially unbounded structures.

struct Node { int value; std::vector<Node> children; };

std::generator<int> walk(const Node& n) {
    co_yield n.value;
    for (const auto& child : n.children)
        for (int v : walk(child))   // рекурсивная делегация co_yield// recursive co_yield delegation
            co_yield v;
}
02

Построчное чтение большого файла без загрузки целиком в памятьReading a large file line by line without loading it entirely into memory

Обработка файла, слишком большого, чтобы держать его целиком в vector<string> — генератор отдаёт по одной строке за раз, потребитель обрабатывает и забывает.Processing a file too large to hold entirely in a vector<string> — the generator hands out one line at a time, and the consumer processes and discards it.

std::generator<std::string> read_lines(const std::filesystem::path& p) {
    std::ifstream in(p);
    std::string line;
    while (std::getline(in, line))
        co_yield line;
}

for (const auto& line : read_lines("huge_log.txt")) {
    process_line(line);   // одна строка в памяти за раз, не весь файл// one line in memory at a time, not the whole file
}
03

Композиция генератора с ranges-адаптерамиComposing a generator with ranges adaptors

Бесконечный генератор простых чисел, отфильтрованный и ограниченный через views — генератор ведёт себя как любой другой view из ranges, ничего специального в цепочке не требуется.An infinite generator of prime numbers, filtered and bounded via views — the generator behaves like any other ranges view, nothing special is needed in the chain.

std::generator<int> primes() {
    for (int n = 2; ; ++n)
        if (is_prime(n)) co_yield n;
}

auto first_five_over_100 = primes()
    | std::views::filter([](int p) { return p > 100; })
    | std::views::take(5);
// см. cpp20/ranges-basics.html — та же композиция, что и с любым другим range// see cpp20/ranges-basics.html — the same composition as with any other range
04

Замена стороннего Generator<T> из cppcoro на std::generatorReplacing a third-party cppcoro Generator<T> with std::generator

Код, ранее зависящий от cppcoro исключительно ради типа Generator<T>, переписанный на стандартный тип — убирает внешнюю зависимость там, где она была нужна только для этой одной возможности.Code that previously depended on cppcoro solely for the Generator<T> type, rewritten to use the standard type — removing the external dependency where it was needed only for this one capability.

// было: #include <cppcoro/generator.hpp>  →  cppcoro::generator<T>// before: #include <cppcoro/generator.hpp>  →  cppcoro::generator<T>
#include <generator>

std::generator<std::pair<int, int>> enumerate_pairs(std::span<const int> values) {
    for (int i = 0; i < static_cast<int>(values.size()); ++i)
        co_yield {i, values[i]};
}
// сигнатура и семантика идентичны, зависимость от cppcoro исчезла// the signature and semantics are identical, the cppcoro dependency is gone

ГраблиGotchas

Ленивый, однопроходный, обычно move-only — не eager-контейнерLazy, single-pass, usually move-only — not an eager container

std::generator нельзя итерировать многократно, как vector или любой другой полноценный контейнер — это одноразовый (single-pass) диапазон: пройдя его один раз до конца, начать заново без повторного вызова функции-генератора нельзя. Сам объект generator обычно move-only, не копируемый. Путать его с "ленивым, но многоразовым" view — источник багов там, где код неявно рассчитывает на повторную итерацию.std::generator cannot be iterated multiple times like a vector or any other proper container — it's a single-pass range: once you've walked it to the end, you can't start over without calling the generator function again. The generator object itself is typically move-only, non-copyable. Confusing it with a "lazy but reusable" view is a source of bugs wherever code implicitly expects to re-iterate.

По умолчанию аллоцирует coroutine frame в куче — не бесплатная абстракцияAllocates the coroutine frame on the heap by default — not a zero-cost abstraction

Как и любая корутина (см. cpp20/coroutines.html), std::generator по умолчанию использует allocator для coroutine frame — та самая heap allocation, которую компилятор МОЖЕТ (но не обязан) устранить через HALO, по прямой параллели с guaranteed copy elision/NRVO: гарантии здесь нет, есть только опциональная оптимизация. Для по-настоящему горячих путей стоит профилировать, а не полагаться на то, что аллокация будет устранена.Like any coroutine (see cpp20/coroutines.html), std::generator uses an allocator for the coroutine frame by default — that same heap allocation the compiler MAY (but isn't obligated to) eliminate via HALO, in direct parallel with guaranteed copy elision/NRVO: there's no guarantee here, only an optional optimization. For truly hot paths, profile rather than assume the allocation will be elided.

Поддержка компиляторами появилась позже самого стандарта — проверять конкретный тулчейнCompiler support arrived later than the standard itself — check the specific toolchain

Как и в истории std::format (стандартизирован в C++20, полноценно доступен в основных компиляторах заметно позже), поддержка std::generator в GCC/Clang/MSVC появлялась постепенно после выхода C++23 — практическая заметка: прежде чем полагаться на std::generator в проекте, стоит явно проверить версию используемого компилятора и стандартной библиотеки, а не полагаться на факт "это C++23, значит доступно".Just as with std::format's history (standardized in C++20, fully available in major compilers noticeably later), support for std::generator in GCC/Clang/MSVC rolled out gradually after C++23 shipped — a practical note: before relying on std::generator in a project, explicitly check the compiler and standard library version in use, rather than assuming "it's C++23, so it's available."