Корутины
C++20 корутины — co_await/co_yield/co_return, coroutine frame, promise_type, awaitable-объекты, генераторы и symmetric transfer.
30 вопросов
JuniorТеорияОчень частоЧто такое std::coroutine_handle и что с ним можно делать?
Что такое std::coroutine_handle и что с ним можно делать?
std::coroutine_handle — тонкая невладеющая обёртка над указателем на coroutine frame. С его помощью можно resume() корутину, проверить done(), вызвать destroy() и обратиться к promise(). Временем жизни фрейма он не управляет.
Типичные ошибки
- ✗Считать, что хэндл владеет фреймом и освобождает его при уничтожении
- ✗Забывать вызвать
destroy(), что приводит к утечке фрейма незавершённой корутины - ✗Путать
done()(достигнут final suspend) с тем, что корутина уже уничтожена
Уточняющие вопросы
- →Как обернуть
coroutine_handle, чтобы фрейм освобождался через RAII? - →В чём разница между
coroutine_handle<Promise>иcoroutine_handle<>?
JuniorТеорияОчень частоЧто делают co_await, co_yield и co_return в корутине?
Что делают co_await, co_yield и co_return в корутине?
co_await expr приостанавливает корутину, пока expr не готово, и отдаёт его результат. co_yield v приостанавливает и передаёт v вызывающей стороне. co_return завершает корутину, опционально передавая результат через promise_type. Любое из трёх делает функцию корутиной.
Типичные ошибки
- ✗Считать, что
co_yieldзавершает корутину так же, какco_return - ✗Думать, что
co_awaitвсегда блокирует поток, а не приостанавливает корутину - ✗Полагать, что функция с этими ключевыми словами ведёт себя как обычная функция
Уточняющие вопросы
- →Почему обычный
returnзапрещён внутри корутины? - →Какому протоколу должен соответствовать операнд
co_await?
JuniorТеорияОчень частоЧто такое promise_type и почему он нужен каждой корутине?
Что такое promise_type и почему он нужен каждой корутине?
promise_type — тип, который компилятор находит по возвращаемому типу и через который настраивается корутина. Он строит возвращаемый объект, решает приостановки в начале и конце, обрабатывает co_yield/co_return и ловит исключения. Без него хуки жизненного цикла не сгенерировать.
Типичные ошибки
- ✗Путать
promise_typeс возвращаемым объектом, который он создаёт для вызывающей стороны - ✗Думать, что
promise_typeнеобязателен или генерируется компилятором автоматически - ✗Считать, что
promise_typeпланирует или запускает корутину в потоке
Уточняющие вопросы
- →Какие методы должен предоставлять
promise_type? - →Как компилятор находит
promise_typeдля конкретной корутины?
JuniorТеорияОчень частоЧто означает «приостановка» корутины?
Что означает «приостановка» корутины?
Приостановка означает, что корутина останавливается в определённой точке, сохраняет локальное состояние и позицию возобновления во фрейм и возвращает управление вызывающей стороне — без раскрутки стека и освобождения фрейма. Позже её можно возобновить с этой точки.
Типичные ошибки
- ✗Путать приостановку корутины с блокировкой или вытеснением потока ОС
- ✗Думать, что приостановка уничтожает локальные переменные или раскручивает стек, как
return - ✗Считать, что приостановленная корутина продолжает работать где-то в фоне
Уточняющие вопросы
- →Что именно сохраняется в coroutine frame в точке приостановки?
- →Кто отвечает за возобновление приостановленной корутины?
JuniorТеорияОчень частоЧем корутины отличаются от потоков?
Чем корутины отличаются от потоков?
Поток — это вытесняемый контекст выполнения, планируемый ОС, со своим стеком, работающий параллельно на другом ядре. Корутина — приостанавливаемая функция с фреймом в куче, планируемая кооперативно; сама по себе она не даёт параллелизма.
Типичные ошибки
- ✗Считать, что корутины дают параллелизм без внешнего планировщика или пула потоков
- ✗Думать, что каждая корутина занимает свой поток ОС
- ✗Полагать, что
co_awaitавтоматически переносит работу в фоновый поток
Уточняющие вопросы
- →Когда выбрать корутину вместо потока и наоборот?
- →Как корутина всё же может со временем выполняться в нескольких потоках?
JuniorТеорияЧастоГде хранится состояние корутины, пока она приостановлена?
Где хранится состояние корутины, пока она приостановлена?
Оно хранится в coroutine frame — сгенерированном компилятором объекте, обычно размещаемом в куче. Фрейм содержит promise_type, точку возобновления, параметры и локальные переменные, переживающие приостановку. Стековый фрейм вызывающей стороны исчезает после первой приостановки.
Типичные ошибки
- ✗Считать, что локальные переменные корутины живут на обычном стеке между приостановками
- ✗Думать, что
coroutine_handleхранит состояние, а не просто указывает на фрейм - ✗Полагать, что фрейм освобождается автоматически в момент приостановки корутины
Уточняющие вопросы
- →Когда coroutine frame выделяется и когда освобождается?
- →Может ли heap-аллокация фрейма быть элиминирована?
JuniorТеорияЧастоЧто такое генератор и каким ключевым словом корутины он создаётся?
Что такое генератор и каким ключевым словом корутины он создаётся?
Генератор — корутина, которая лениво производит последовательность значений, по одному на возобновление, а не вычисляет их заранее. Он строится через co_yield, который приостанавливает и передаёт текущее значение вызывающей стороне. C++23 добавляет std::generator<T>.
Типичные ошибки
- ✗Думать, что генератор вычисляет все значения сразу, а не по требованию
- ✗Связывать генераторы с
co_awaitвместоco_yield - ✗Считать, что генератор запускает производителя в отдельном потоке
Уточняющие вопросы
- →Что делает
promise_type::yield_valueдля генератора? - →Почему генераторы обычно используют
suspend_alwaysдляinitial_suspend?
JuniorТеорияЧастоПочему нельзя использовать обычный return внутри корутины?
Почему нельзя использовать обычный return внутри корутины?
Корутина не возвращает значение обычным способом — её результат должен проходить через promise_type посредством return_value/return_void. Обычный return обходит этот механизм, поэтому язык его запрещает; нужен co_return, который разворачивается в вызов promise.
Типичные ошибки
- ✗Думать, что
co_returnиreturnвзаимозаменяемы внутри корутины - ✗Не понимать, что результат должен проходить через методы
promise_type - ✗Считать, что функция с
co_awaitможет ещё использовать и обычныйreturn
Уточняющие вопросы
- →Когда
co_returnвызываетreturn_value, а когдаreturn_void? - →Что происходит после
co_return— когда уничтожается фрейм?
JuniorТеорияЧастоВ чём разница между std::suspend_always и std::suspend_never?
В чём разница между std::suspend_always и std::suspend_never?
Оба — тривиальные стандартные awaitable. У std::suspend_always метод await_ready() возвращает false, поэтому co_await всегда приостанавливает. У std::suspend_never он возвращает true, поэтому co_await не приостанавливает. Результата ни один не несёт.
Типичные ошибки
- ✗Думать, что
suspend_neverблокирует или уступает поток, а не просто не приостанавливает - ✗Считать, что выбор влияет на каждый оператор, а не на одну точку
co_await - ✗Путать их при выборе
initial_suspendдля жадного или ленивого старта
Уточняющие вопросы
- →Как возвращение каждого из них из
initial_suspendменяет момент старта корутины? - →Какие три метода должен предоставлять любой awaitable, включая эти?
MiddleТеорияЧастоКакие три вызова генерирует компилятор для co_await expr?
Какие три вызова генерирует компилятор для co_await expr?
Компилятор генерирует три вызова Awaitable: await_ready() решает, нужна ли приостановка; await_suspend(handle) выполняется при припаркованном фрейме; await_resume() даёт значение выражения при возобновлении.
Типичные ошибки
- ✗Считать, что
await_suspendвсегда приостанавливает — приawait_ready()равномtrueприостановка пропускается - ✗Думать, что
await_resume()вызывается только в отдельном потоке, а не в том, что возобновляет фрейм - ✗Полагать, что значение выражения
co_awaitберётся изexpr, а не изawait_resume()
Уточняющие вопросы
- →Что меняет возвращаемый тип
await_suspendв возобновлении? - →Как
operator co_awaitилиawait_transformвстраиваются в эту развёртку?
MiddleТеорияЧастоПереносит ли co_await работу корутины в другой поток?
Переносит ли co_await работу корутины в другой поток?
Нет. co_await лишь приостанавливает корутину и возвращает управление вызывающему — поток он не порождает. Возобновление идёт в потоке, вызвавшем handle.resume(). Смена потока требует планирующего Awaitable.
Типичные ошибки
- ✗Считать, что корутины работают параллельно в фоне без всякого внешнего планировщика
- ✗Называть корутины 'асинхронной' возможностью и ждать параллелизма бесплатно
- ✗Ожидать, что
co_awaitснимет CPU-нагрузку с текущего потока
Уточняющие вопросы
- →Какой Awaitable действительно возобновил бы корутину в пуле потоков?
- →Почему именно приостановка, а не асинхронность, определяет корутину?
MiddleТеорияЧастоДолжна ли корутина начинать выполнение при вызове или только при первом возобновлении?
Должна ли корутина начинать выполнение при вызове или только при первом возобновлении?
Это ваш выбор, задаваемый через initial_suspend. suspend_never делает корутину жадной — при вызове она выполняется до первой приостановки. suspend_always делает её ленивой — она стартует при первом resume(). Генераторы обычно ленивы.
Типичные ошибки
- ✗Думать, что стандарт фиксирует момент старта, а не оставляет его на
initial_suspend - ✗Считать, что фрейм не выделяется до первого
resume() - ✗Путать
initial_suspendсfinal_suspendпри рассуждении о старте и завершении
Уточняющие вопросы
- →Какие проблемы создаёт жадно стартующая корутина для обработки ошибок?
- →Почему ленивый старт — естественный выбор для генератора?
MiddleТеорияЧастоЧем управляет final_suspend и почему обычно это suspend_always?
Чем управляет final_suspend и почему обычно это suspend_always?
final_suspend() выполняется после тела и решает, останется ли фрейм живым. suspend_always сохраняет его, чтобы вызывающий прочитал результат или исключение; suspend_never уничтожил бы его сразу, оставив висячий handle.
Типичные ошибки
- ✗Путать
final_suspendсinitial_suspend— один выполняется после тела, другой до него - ✗Вернуть
suspend_neverи затем читать результат через уже уничтоженный фрейм - ✗Забыть, что
final_suspend()обязан бытьnoexcept, иначе программа некорректна
Уточняющие вопросы
- →Кто отвечает за уничтожение фрейма, когда
final_suspendвернулsuspend_always? - →Как
final_suspendможет через symmetric transfer возобновить ожидающую корутину?
MiddleКодЧастоКак реализовать минимальный генератор без std::generator?
Как реализовать минимальный генератор без std::generator?
Объявите тип с вложенным promise_type, чей yield_value сохраняет значение и возвращает suspend_always. Оберните coroutine_handle, дайте next()/value() и вызовите destroy() фрейма в деструкторе.
Типичные ошибки
- ✗Вернуть
suspend_neverизyield_value, из-за чего корутина не приостанавливается наco_yield - ✗Забыть
destroy()в деструкторе, что приводит к утечке heap-фрейма - ✗Опустить
noexceptуfinal_suspend(), делая программу некорректной
Уточняющие вопросы
- →Как дать генератору настоящие итераторы для range-based
for? - →Что добавляет
std::generatorповерх этой ручной версии в C++23?
MiddleТеорияЧастоКакие методы обязан предоставлять promise_type?
Какие методы обязан предоставлять promise_type?
Он обязан дать get_return_object, initial_suspend, final_suspend (noexcept), unhandled_exception и ровно один из return_value либо return_void. Генератору нужен ещё yield_value. Отсутствие любого обязательного члена — некорректность.
Типичные ошибки
- ✗Забывать, что
final_suspendдолжен быть объявленnoexcept - ✗Определять и
return_value, иreturn_voidв одном promise - ✗Путать обязательные методы promise с тремя методами awaitable
Уточняющие вопросы
- →Почему
final_suspendобязан бытьnoexcept? - →Что пойдёт не так, если определить и
return_value, иreturn_void?
MiddleТеорияЧастоСемантически, чем co_yield отличается от co_return?
Семантически, чем co_yield отличается от co_return?
co_yield v вызывает promise.yield_value(v) и приостанавливает корутину — её можно возобновить снова, порождая последовательность. co_return вызывает return_value/return_void и завершает корутину навсегда.
Типичные ошибки
- ✗Думать, что корутину можно возобновить после
co_return— нельзя, тело завершено - ✗Считать, что
co_yieldзавершает корутину как обычный операторreturn - ✗Полагать, что
co_yield x— это просто сахар дляco_return xвнутри цикла
Уточняющие вопросы
- →Какой awaitable возвращает
yield_valueи почему обычно этоsuspend_always? - →Может ли одна корутина использовать и
co_yield, иco_return?
SeniorДизайнЧастоВы выбираете, как организовать асинхронную работу в C++-сервисе. Сравните четыре подхода — обычные callbacks, std::future, корутины C++20 и модель структурной конкурентности senders/receivers из C++26 — по читаемости, композиции зависимых шагов и распространению ошибок.
Вы выбираете, как организовать асинхронную работу в C++-сервисе. Сравните четыре подхода — обычные callbacks, std::future, корутины C++20 и модель структурной конкурентности senders/receivers из C++26 — по читаемости, композиции зависимых шагов и распространению ошибок.
Callbacks просты, но вложены. Futures выглядят синхронно, но композиция неудобна. Корутины C++20 дают линейный co_await; senders/receivers C++26 добавляют schedulers.
Типичные ошибки
- ✗Смешивать callback и future в одной библиотеке — хрупкая обработка ошибок
- ✗Использовать дефолты
std::async(std::launch::async | std::launch::deferred) — неожиданный lazy execution - ✗Захватывать ссылки в корутинах — dangling после suspend
Уточняющие вопросы
- →Почему
std::futureплох для композиции по сравнению сfolly::Future? - →Как выглядит cancellation в senders/receivers?
SeniorТеорияЧастоЧто превращает обычную функцию в корутину C++20?
Что превращает обычную функцию в корутину C++20?
Функция становится корутиной, если её тело использует co_await, co_yield или co_return. Компилятор превращает её в state machine с heap-аллоцированным frame. Тип возврата должен определять promise_type.
Типичные ошибки
- ✗Захват ссылок в корутине — могут стать dangling после suspend
- ✗Забывать, что handle владеет frame — неправильное уничтожение даёт утечку
- ✗Путать
co_yield(suspend с значением) иco_return(завершение корутины)
Уточняющие вопросы
- →Что контролирует
promise_type::initial_suspend? - →Как
std::generator(C++23) оборачивает корутинную машину?
SeniorТеорияЧастоЧто такое корутины C++20? co_await, co_yield, co_return.
Что такое корутины C++20? co_await, co_yield, co_return.
Корутины C++20 приостанавливают и возобновляют выполнение без блокирования потока. co_await/co_yield/co_return делают функцию корутиной; компилятор превращает её в state machine на куче. Тип возврата требует пользовательский promise.
Типичные ошибки
- ✗Ожидать готовых типов корутин в C++20 — стандарт предоставляет только механизм; нужен promise-тип, который нужно написать самому или взять из библиотеки (C++23 добавляет
std::generator) - ✗Путать корутины с многопоточностью — корутины кооперативные и однопоточные по умолчанию; параллелизм требует явного планирования на пуле потоков
- ✗Висячие ссылки во фреймах корутин — если корутина захватывает локальную переменную по ссылке, а вызывающий уничтожает её до возобновления, это UB; предпочтительно захватывать по значению или через shared ownership
Уточняющие вопросы
- →Что делает
co_await std::suspend_always{}по сравнению сco_await std::suspend_never{}? - →Как promise-тип управляет выделением памяти для фрейма корутины (elision operator new)?
MiddleПроизводительностьИногдаКогда coroutine frame выделяется и освобождается, и может ли аллокация быть элиминирована?
Когда coroutine frame выделяется и освобождается, и может ли аллокация быть элиминирована?
Фрейм выделяется (обычно в куче) один раз при первом вызове корутины и освобождается при вызове destroy(). Компилятор может элиминировать heap-аллокацию (HALO), когда время жизни корутины ему полностью видимо.
Типичные ошибки
- ✗Считать, что фрейм переаллоцируется на каждом resume, а не один раз при вызове
- ✗Думать, что фрейм освобождается сам при
co_return, а не приdestroy() - ✗Полагать, что HALO происходит всегда, а не только при полностью видимом времени жизни
Уточняющие вопросы
- →При каких условиях компилятор может применить HALO и встроить фрейм?
- →Как задать кастомный аллокатор для coroutine frame?
MiddleТеорияИногдаКак получить coroutine_handle изнутри promise и зачем?
Как получить coroutine_handle изнутри promise и зачем?
Вызовите std::coroutine_handle<promise_type>::from_promise(*this) — он восстанавливает handle из адреса promise, так как promise лежит по известному смещению во фрейме. get_return_object() строит этим обёртку вызывающего.
Типичные ошибки
- ✗Вызывать
from_promiseс promise, который на деле не член coroutine frame - ✗Путать
from_promise(promise в handle) сfrom_address(сырой указатель в handle) - ✗Думать, что handle доступен только вызывающему, но не самому promise
Уточняющие вопросы
- →Почему
from_promiseиpromise()должны быть точными обратными друг другу? - →Какое UB возникает, если передать в
from_promisepromise не из фрейма?
MiddleДебаггингИногдаКак корутина может утечь по памяти и как это предотвратить?
Как корутина может утечь по памяти и как это предотвратить?
coroutine_handle не владеет фреймом. Если потерять хэндл незавершённой корутины или не вызвать destroy(), фрейм утекает. Предотвращается обёрткой хэндла в RAII-тип, чей деструктор вызывает destroy(), и корректной move-семантикой, чтобы владение оставалось уникальным.
Типичные ошибки
- ✗Считать, что
coroutine_handleвладеет фреймом или считает на него ссылки - ✗Копировать хэндл так, что оба владельца вызывают
destroy()— двойное освобождение - ✗Думать, что
final_suspendсsuspend_alwaysсам выполняет очистку
Уточняющие вопросы
- →Что пойдёт не так, если
final_suspendвернётsuspend_never? - →Как обнаружить утёкший coroutine frame с помощью санитайзера?
MiddleКодИногдаПочему параметры корутины обычно следует передавать по значению?
Почему параметры корутины обычно следует передавать по значению?
Параметр по значению копируется в coroutine frame, поэтому он живёт через приостановки всё время жизни корутины. Ссылочный параметр лишь указывает на объект вызывающей стороны — если она вернётся раньше, ссылка становится висячей, что является undefined behavior.
Открыть задачу →Типичные ошибки
- ✗Думать, что ссылочный параметр копируется в фрейм, как параметр по значению
- ✗Считать, что стековый фрейм вызывающей стороны живёт до завершения корутины
- ✗Полагать, что компилятор автоматически отвергает или переписывает ссылочные параметры
Уточняющие вопросы
- →Безопасна ли захватывающая лямбда, используемая как корутина, через приостановки?
- →Когда ссылочный параметр в корутине всё же приемлем?
MiddleДебаггингИногдаКак наивное связывание resume() может переполнить стек?
Как наивное связывание resume() может переполнить стек?
Если await_suspend вызывает handle.resume() для следующей корутины, а та возобновляет ещё одну, каждый resume() вкладывается в предыдущий стековый фрейм. Длинная цепочка не разматывается и переполняет стек; фреймы в куче этого не меняют.
Типичные ошибки
- ✗Вызывать
handle.resume()внутриawait_suspendвместо возврата handle - ✗Думать, что heap-фреймы означают, что цепочки корутин не переполнят стек
- ✗Считать опасность лишь теоретической и игнорировать глубокие цепочки producer/consumer
Уточняющие вопросы
- →Как возврат
coroutine_handle<>изawait_suspendограничивает глубину стека? - →Почему хвостовой вызов — ключевое отличие безопасного возобновления от опасного?
MiddleТеорияИногдаЧто происходит, когда исключение покидает тело корутины?
Что происходит, когда исключение покидает тело корутины?
Исключение, покинувшее тело, перехватывается компилятором и идёт в promise.unhandled_exception(). В стек возобновляющего кода оно не попадает. Типичный promise сохраняет std::current_exception() и перебрасывает позже из get().
Типичные ошибки
- ✗Оборачивать
handle.resume()вtry/catchв надежде поймать там исключение тела - ✗Забыть сохранить исключение в promise, из-за чего
get()молча вернёт устаревшее значение - ✗Бросать исключение из самого
unhandled_exception(), что ведёт кstd::terminate
Уточняющие вопросы
- →Почему сам
unhandled_exception()не должен выпускать исключение наружу? - →Как
Taskперебрасывает сохранённое исключение своему awaiter?
SeniorТеорияИногдаЧто означают разные возвращаемые типы await_suspend?
Что означают разные возвращаемые типы await_suspend?
void приостанавливает и возвращает управление вызывающему. bool делает то же при true, но false сразу возобновляет корутину. Возврат coroutine_handle<> — это symmetric transfer: handle возобновляется без роста стека.
Типичные ошибки
- ✗Думать, что
falseизawait_suspendотменяет корутину, а не возобновляет её - ✗Считать, что возвращённый
coroutine_handle<>просто возобновится позже, а не получит управление сразу - ✗Полагать, что
voidиbool trueразличаются — оба приостанавливают и отдают управление вызывающему
Уточняющие вопросы
- →Почему перегрузка с
coroutine_handle<>критична для цепочек awaiter? - →Что будет, если
await_suspendвозобновит handle и при этом ещё вернёт handle?
SeniorКодИногдаКак написать кастомный Awaitable, возобновляющий корутину при завершении I/O?
Как написать кастомный Awaitable, возобновляющий корутину при завершении I/O?
Сделайте await_ready() возвращающим false, пусть await_suspend(handle) регистрирует I/O и сохраняет handle в колбэке завершения, а await_resume() возвращает результат. Колбэк вызывает handle.resume().
Типичные ошибки
- ✗Вернуть
trueизawait_ready(), из-за чего корутина не приостанавливается для ожидания I/O - ✗Захватить
handleпо ссылке в колбэке — он повиснет после возвратаawait_suspend - ✗Возобновить handle до сохранения результата, из-за чего
await_resume()читает мусор
Уточняющие вопросы
- →Почему безопасно возобновлять handle из потока I/O, а не из исходного?
- →Как пробросить ошибку I/O наружу через
await_resume()?
SeniorДебаггингИногдаКак корутина может породить висячую ссылку и когда это стреляет?
Как корутина может породить висячую ссылку и когда это стреляет?
Параметр корутины по ссылке не копируется во фрейм — копируется лишь ссылка. Если объект вызывающего умирает до возобновления, его использование — UB. Стреляет после первой приостановки, когда стек вызывающего размотан.
Открыть задачу →Типичные ошибки
- ✗Считать, что все параметры корутины глубоко копируются во фрейм, включая ссылки
- ✗Передать временный объект в параметр-ссылку корутины и использовать его после приостановки
- ✗Захватить ссылку на локальную вызывающего и возобновить корутину после его возврата
Уточняющие вопросы
- →Почему передача параметра по значению делает фрагмент безопасным?
- →Как эта опасность сочетается с дефолтными аргументами, являющимися временными?
SeniorТеорияИногдаКак компилятор представляет точку возобновления приостановленной корутины?
Как компилятор представляет точку возобновления приостановленной корутины?
Компилятор переписывает тело в конечный автомат внутри heap-фрейма. Целое — индекс возобновления — хранит, с какой точки продолжать; resume() диспетчеризует по нему (по сути switch), прыгая в нужное место с сохранёнными локальными.
Типичные ошибки
- ✗Считать coroutine frame снимком регистров/стека, а не сгенерированным конечным автоматом
- ✗Думать, что возобновление — это переключение контекста ОС, а не индексированный переход
- ✗Полагать, что размер фрейма динамический; он фиксирован и вычислен на этапе компиляции
Уточняющие вопросы
- →Почему компилятор иногда может полностью элиминировать heap-аллокацию фрейма?
- →Какие локальные хранятся во фрейме, а какие остаются в регистрах через приостановку?
SeniorТеорияРедкоЧто такое symmetric transfer и какую проблему он решает?
Что такое symmetric transfer и какую проблему он решает?
Symmetric transfer — это возврат coroutine_handle<> из await_suspend, чтобы компилятор возобновил корутину хвостовым вызовом вместо вложенного resume(). Это решает переполнение стека при цепочках корутин.
Типичные ошибки
- ✗Путать symmetric transfer с многопоточностью — он целиком однопоточный
- ✗Думать, что это оптимизация скорости, тогда как его цель — ограничить глубину стека
- ✗Вызывать
handle.resume()внутриawait_suspendвместо возврата handle, теряя весь смысл
Уточняющие вопросы
- →Что такое
std::noop_coroutineи когда возвращать его изawait_suspend? - →Почему вложенная цепочка
resume()растит стек, а handle, возвращённый хвостом, нет?