В интернете невероятное количество статей о том "как написать свой генератор на С++20", но почти все они сводятся к новичковым хело вордам и почти ни одной статьи о том как написать хороший генератор. Что ж, это нужно исправлять! в С++20 добавили корутины(условно говоря функции, которые могут приостанавливать выполнение и продолжать), а генератор это одна из простейших корутин, он просто генерирует значения, причём всегда синхронно. Пример:  Сначала определимся что такое хороший генератор, для меня это когда он: эффективен, мы всё таки на С++ пишем удобен в использовании(не провоцирует неявных ошибок, интерфейс понятен даже новичку) В связи с этим возникает вопрос, зачем писать свой, ведь в С++23 уже добавили std::generator? Ну, во-первых С++23 в проде будет ещё не скоро, а во-вторых std::generator это, к сожалению, одна из худших вещей добавленных в стандартную библиотеку. Вкратце опишу свои претензии к нему(кстати, его невозможно реализовать без компиляторной магии) Интерфейс   Первый = void означает "по дефолту", второй = void означает type erasure! Это сбивает с толку даже матёрого С++ программиста и даже после чтения документации.  Сами авторы признают, что Alloc в этом интерфейсе лишний - логика генератора никак не зависит от того как его аллоцировали. Это должно достигаться специализацией coroutine_traits. Ну это ещё полбеды, вот вы захотели генератор строк. Пишем Это уже будет неэффективно, потому что первый аргумент шаблона - ссылка. Правильно было бы написать generator<string_view>, но тогда на принимающей стороне нужно копировать. В итоге у вас выйдет что то типа generator<string&&, string> или что похуже и одному богу известно будет ли это эффективно.  std::generator буквально неюзабелен. Пройдусь банально по всем его методам: begin - нельзя вызывать дважды, при этом нет способа проверить вызван ли он уже. Вызовете второй раз - undefined behavior. Хотя ожидаемое лично мной поведение - продолжение итерации с того места, где остановились в предыдущий раз end - возвращает std::default_sentinel всегда, при этом метод не является статическим деструктор - неявно noexcept, хотя .destroy на хендле который он вызывает может бросить исключение при разрушении локальных объектов на корутине. То есть помимо UB вам подсунули ещё и неявный std::terminate мув конструктор - проблема в том, что он есть. А дефолтного конструктора нет! Это нонсенс, таким образом после мува конструктор является invalid объектом, который использовать нельзя. Никак. Вообще. std::pmr::generator - проблема опять же в том, что это существует. Это алиас на генератор с  std::pmr::polymorphic_alloc, хотя генератор не является контейнером и не использует этот аллокатор буквально никак, не вдаваясь в подробности - создание этого алиаса это банальный обман пользователя   Ух, что то я увлёкся...  Итак, мы определились с тем что хотим видеть в генераторе и чего не хотим, для реализации нам понадобится написать promise type нашей корутины с возможностью yield значение  сущность generator управляющую через промис корутиной итератор, который будет ссылаться на генератор Очевидно, что главное что надо сделать это yield и вы можете найти тысячи реализаций в интернете, которые выглядят как-то так: И это .. неэффективно! Зачем нам хранить и создавать значение, если можно просто взять на него указатель? Каждый уважающий себя программист на С++ сразу же, как ему покажется, увидит ошибку. Мы сохраняем указатель на "временный" объект, который разрушится как только вызов yield_value закончится Но вы забыли, это же корутины! Давайте посмотрим на это из генератора Поэтому, если вспомнить правило "временные значения живут до конца выражения" и цитату стандарта "co_yield x" это yield-expression можно понять, что сохранять указатель это абсолютно безопасно и главное - эффективно. Теперь насчёт других специальных методов промиса: мы должны решить как будет вести себя генератор на старте, генерировать первое значение сразу или подождать? Тут решение на самом деле неочевидно, но в итоге все реализации приходят к приостановке перед генерацией, чтобы в .begin сгенерировать первое значение и не пропустить ничего... А ещё чтобы генератор не стал за эту первую свою итерацию рекурсивным, сломав что-нибудь по дороге (но о рекурсивности - позже)  Для этого нужно немного подумать над тем, как наш генератор будут использовать. Представим себе такой код: Генератор выбрасывает значения другого генератора. Что при этом происходит? пробуждается генератор 'g' он будит генератор 'another_generator' тот будит какой то ещё генератор и так по цепочке O(N) раз, в конце концов получается значение. Каждое из пробуждений - достаточно дорогая операция, сравнимая с вызовом функции по указателю А что если мы будем сразу из самого верхнего генератора в цепочке пробуждать самый нижний, а тот когда сгенерирует значение будет будить опять самый верхний? В этом и кроется суть рекурсивного генератора, он превращает O(N) вызовов для получения значения в гарантированное O(1) и это прекрасно. Но для реализации подобного поведения нам придётся написать интрузивный список генераторов, а если захочется эффективности, то ещё ооочень много self reference типов промисов. Но главное - что получается в конце. Нет буквально ни одного лишнего if относительно обычного генератора, весь оверхед - память, 1-2 указателя на 1 генератор. Фактически - оверхед смешной учитывая какие плюсы он даёт. Идея будет простой: если генератор  видит yield другого генератора, то он подключает его в цепочку, говорит ему куда генерировать значения и ... передаёт управление симметричным трансфером. Название сложное, а на деле это просто пробуждение другого генератора. Тот в свою очередь в final_suspend вернёт управление обратно  Тут мы вводим несколько новых сущностей: current_worker - генератор, который сейчас активен(самый нижний в цепочке) root - самый высокий в цепочке генератор owner - генератор, в котором yield'нули генератор Самое интерерсное здесь, пожалуй, то как устроен промис То есть root по умолчанию это сам генератор, current_worker по умолчанию - тоже он же сам, current_result_ptr вовсе указывает на generator, то есть активный генератор прямо сразу туда складывает значения, минуя вообще всех Интерфейс нашего генератора: Мы получили генератор, который эффективен, рекурсивен, имеет дефолтный конструктор и вызов цикл for по дефолтно сконструированному генератору даст 0 значений(напомню std::generator даст UB), можно вызывать begin повторно и просто продолжать итерацию,  Ещё очень много тонкостей с обработкой исключений, кастомизацией аллокаций и мелочами для эффективности, всё это вы можете посмотреть в исходном коде: https://github.com/kelbon/kelcoro