Coroutines (C++20)
Сопрограмма — это функция, которая может приостановить выполнение для последующего возобновления. Сопрограммы бесстековые: они приостанавливают выполнение, возвращая управление вызывающей стороне, а данные, необходимые для возобновления выполнения, хранятся отдельно от стека. Это позволяет последовательному коду выполняться асинхронно (например, для обработки неблокирующего ввода-вывода без явных обратных вызовов), а также поддерживает алгоритмы для лениво вычисляемых бесконечных последовательностей и другие применения.
Функция является сопрограммой, если её определение содержит любое из следующего:
- выражение co_await — приостанавливает выполнение до возобновления
task<> tcp_echo_server() { char data[1024]; while (true) { std::size_t n = co_await socket.async_read_some(buffer(data)); co_await async_write(socket, buffer(data, n)); } }
- выражение co_yield — для приостановки выполнения с возвратом значения
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- оператор co_return — для завершения выполнения с возвратом значения
lazy<int> f() { co_return 7; }
Каждая сопрограмма должна иметь тип возврата, удовлетворяющий ряду требований, указанных ниже.
Содержание |
Ограничения
Сопрограммы не могут использовать
вариативные аргументы
, обычные
операторы return
, или
типы возвращаемых значений-заполнители
(
auto
или
Концепты
).
Consteval функции , constexpr функции , конструкторы , деструкторы , и функция main не могут быть корутинами.
Выполнение
Каждая сопрограмма связана с
- объект promise , управляемый изнутри корутины. Корутина передает свой результат или исключение через этот объект. Объекты promise никак не связаны с std::promise .
- дескриптор корутины , управляемый извне корутины. Это невладеющий дескриптор, используемый для возобновления выполнения корутины или уничтожения фрейма корутины.
- состояние корутины , которое является внутренним, динамически выделяемым хранилищем (если выделение не оптимизировано), объектом, содержащим
-
- объект promise
- параметры (все копируются по значению)
- некоторое представление текущей точки приостановки, чтобы resume знал, где продолжить, а destroy знал, какие локальные переменные были в области видимости
- локальные переменные и временные объекты, время жизни которых охватывает текущую точку приостановки.
Когда сопрограмма начинает выполнение, она выполняет следующее:
- выделяет объект состояния сопрограммы с помощью operator new .
- копирует все параметры функции в состояние сопрограммы: параметры по значению перемещаются или копируются, параметры по ссылке остаются ссылками (таким образом, могут стать висячими, если сопрограмма возобновляется после завершения времени жизни объекта — см. примеры ниже).
- вызывает конструктор для объекта promise. Если тип promise имеет конструктор, принимающий все параметры сопрограммы, вызывается этот конструктор с пост-копированными аргументами сопрограммы. В противном случае вызывается конструктор по умолчанию.
- вызывает promise. get_return_object ( ) и сохраняет результат в локальной переменной. Результат этого вызова будет возвращен вызывающей стороне при первой приостановке сопрограммы. Любые исключения, выброшенные до и включая этот шаг, передаются обратно вызывающей стороне, а не помещаются в promise.
-
вызывает
promise.
initial_suspend
(
)
и
co_awaitего результат. Типичные типыPromiseлибо возвращают std::suspend_always для лениво запускаемых сопрограмм, либо std::suspend_never для активно запускаемых сопрограмм. - когда co_await promise. initial_suspend ( ) возобновляется, начинает выполнение тела сопрограммы.
Некоторые примеры, когда параметр становится висячим:
#include <coroutine> #include <iostream> struct promise; struct coroutine : std::coroutine_handle<promise> { using promise_type = ::promise; }; struct promise { coroutine get_return_object() { return {coroutine::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; struct S { int i; coroutine f() { std::cout << i; co_return; } }; void bad1() { coroutine h = S{0}.f(); // S{0} уничтожен h.resume(); // возобновленная сопрограмма выполняет std::cout << i, использует S::i после освобождения h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // возвращенная сопрограмма не может быть возобновлена без использования после освобождения } void bad3() { coroutine h = [i = 0]() -> coroutine // лямбда, которая также является сопрограммой { std::cout << i; co_return; }(); // немедленно вызывается // лямбда уничтожена h.resume(); // использует (анонимный тип лямбды)::i после освобождения h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // сделать i параметром сопрограммы { std::cout << i; co_return; }(0); // лямбда уничтожена h.resume(); // нет проблем, i был скопирован в кадр сопрограммы // как параметр по значению h.destroy(); }
Когда сопрограмма достигает точки приостановки
- ранее полученный объект возврата передаётся вызывающей стороне/возобновителю после неявного преобразования в тип возврата корутины, если это необходимо.
Когда сопрограмма достигает оператора co_return , она выполняет следующие действия:
- вызывает promise. return_void ( ) для
-
- co_return ;
- co_return expr ; где expr имеет тип void
- или вызывает promise. return_value ( expr ) для co_return expr ; где expr имеет тип, отличный от void
- уничтожает все переменные с автоматической продолжительностью хранения в порядке, обратном их созданию.
- вызывает promise. final_suspend ( ) и co_await ожидает результат.
Падение с конца сопрограммы эквивалентно
co_return
;
, за исключением того, что поведение не определено, если в области видимости
Promise
не найдено объявлений
return_void
. Функция без ключевых слов определения в теле функции не является сопрограммой, независимо от её возвращаемого типа, и падение с конца приводит к неопределённому поведению, если возвращаемый тип не является (возможно, cv-квалифицированным)
void
.
// предполагая, что task - это некоторый тип задачи корутины task<void> f() { // не корутина, неопределенное поведение } task<void> g() { co_return; // OK } task<void> h() { co_await g(); // OK, неявный co_return; }
Если сопрограмма завершается неперехваченным исключением, она выполняет следующее:
- перехватывает исключение и вызывает promise. unhandled_exception ( ) внутри блока catch
- вызывает promise. final_suspend ( ) и co_await для результата (например, для возобновления продолжения или публикации результата). Возобновление сопрограммы с этой точки является неопределённым поведением.
Когда состояние сопрограммы уничтожается либо из-за завершения через co_return или неперехваченного исключения, либо потому что оно было уничтожено через свой handle, оно выполняет следующее:
- вызывает деструктор объекта promise.
- вызывает деструкторы копий параметров функции.
- вызывает operator delete для освобождения памяти, используемой состоянием корутины.
- возвращает выполнение вызывающему/возобновившему.
Динамическое выделение памяти
Состояние сопрограммы выделяется динамически с помощью не-массивного operator new .
Если тип
Promise
определяет замену на уровне класса, она будет использована, в противном случае будет использован глобальный
operator new
.
Если тип
Promise
определяет размещающую форму
operator new
, которая принимает дополнительные параметры, и они соответствуют списку аргументов, где первый аргумент — запрашиваемый размер (типа
std::size_t
), а остальные — аргументы функции-корутины, эти аргументы будут переданы в
operator new
(это позволяет использовать
конвенцию с ведущим аллокатором
для корутин).
Вызов operator new может быть исключен оптимизацией (даже при использовании пользовательского аллокатора), если
- Время жизни состояния сопрограммы строго вложено во время жизни вызывающей стороны, и
- размер фрейма сопрограммы известен в точке вызова.
В этом случае состояние сопрограммы встроено в стековый фрейм вызывающей стороны (если вызывающая сторона является обычной функцией) или состояние сопрограммы (если вызывающая сторона является сопрограммой).
Если выделение памяти завершается неудачей, сопрограмма выбрасывает
std::bad_alloc
, за исключением случая, когда тип
Promise
определяет функцию-член
Promise
::
get_return_object_on_allocation_failure
(
)
. Если эта функция-член определена, выделение памяти использует nothrow-форму
operator new
, и при неудачном выделении памяти сопрограмма немедленно возвращает объект, полученный из
Promise
::
get_return_object_on_allocation_failure
(
)
вызывающей стороне, например:
struct Coroutine::promise_type { /* ... */ // обеспечить использование непорождающего исключения operator-new static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // или return Coroutine(nullptr); } // пользовательская непорождающая исключения перегрузка new void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // ошибка выделения памяти } };
Промис
Тип
Promise
определяется компилятором из возвращаемого типа корутины с использованием
std::coroutine_traits
.
Формально, пусть
-
RиArgs...обозначают тип возвращаемого значения и список типов параметров корутины соответственно, -
ClassTобозначает тип класса, к которому принадлежит корутина, если она определена как нестатическая функция-член, - cv обозначает cv-квалификацию, объявленную в объявлении функции если она определена как нестатическая функция-член,
его
Promise
тип определяется следующим образом:
- std:: coroutine_traits < R, Args... > :: promise_type , если сопрограмма не определена как неявная функция-член объекта ,
-
std::
coroutine_traits
<
R,
cvClassT & , Args... > :: promise_type , если сопрограмма определена как неявная функция-член объекта без квалификатора ссылки на rvalue, -
std::
coroutine_traits
<
R,
cvClassT && , Args... > :: promise_type , если сопрограмма определена как неявная функция-член объекта с квалификатором ссылки на rvalue.
Например:
| Если корутина определена как ... |
тогда её тип
Promise
это ...
|
|---|---|
| task < void > foo ( int x ) ; | std:: coroutine_traits < task < void > , int > :: promise_type |
| task < void > Bar :: foo ( int x ) const ; | std:: coroutine_traits < task < void > , const Bar & , int > :: promise_type |
| task < void > Bar :: foo ( int x ) && ; | std:: coroutine_traits < task < void > , Bar && , int > :: promise_type |
co_await
Унарный оператор co_await приостанавливает сопрограмму и возвращает управление вызывающей стороне.
co_await
выражение
|
|||||||||
Выражение co_await может появляться только в потенциально вычисляемом выражении внутри обычного тела функции (включая тело функции лямбда-выражения ), и не может появляться
- в обработчике ,
- в объявительной инструкции, если она не появляется в инициализаторе этой объявительной инструкции,
-
в
простом объявлении
init-statement
(см.
if,switch,forи [[../range- for |range- for ]]), если она не появляется в инициализаторе этого init-statement , - в аргументе по умолчанию , или
- в инициализаторе блочной переменной со статической или потоковой продолжительностью хранения .
|
Выражение co_await не может быть потенциально вычисляемым подвыражением предиката контрактного утверждения . |
(since C++26) |
Сначала expr преобразуется в awaitable следующим образом:
- если expr получено из точки начальной приостановки, точки конечной приостановки или выражения yield, то awaitable объект представляет собой expr в исходном виде.
-
в противном случае, если тип
Promiseтекущей корутины имеет функцию-членawait_transform, то awaitable объект представляет собой promise. await_transform ( expr ) . - в противном случае, awaitable объект представляет собой expr в исходном виде.
Затем объект awaiter получается следующим образом:
- если разрешение перегрузки для operator co_await дает единственную наилучшую перегрузку, то объект ожидания является результатом этого вызова:
-
- awaitable. operator co_await ( ) для перегрузки члена,
- operator co_await ( static_cast < Awaitable && > ( awaitable ) ) для перегрузки не члена.
- в противном случае, если разрешение перегрузки не находит оператор co_await , объект ожидания используется как есть.
- в противном случае, если разрешение перегрузки неоднозначно, программа является некорректной.
Если приведённое выше выражение является prvalue , объект awaiter представляет собой временный объект, материализованный из него. В противном случае, если приведённое выше выражение является glvalue , объект awaiter является объектом, на который оно ссылается.
Затем вызывается awaiter. await_ready ( ) (это сокращение позволяет избежать затрат на приостановку, если известно, что результат готов или может быть завершён синхронно). Если его результат, контекстно преобразованный в bool , равен false , тогда
- Сопрограмма приостанавливается (её состояние заполняется локальными переменными и текущей точкой приостановки).
-
awaiter.
await_suspend
(
handle
)
вызывается, где handle - это дескриптор сопрограммы, представляющий текущую сопрограмму. Внутри этой функции приостановленное состояние сопрограммы доступно для наблюдения через этот дескриптор, и эта функция отвечает за планирование её возобновления в некотором исполнителе или уничтожения (возврат false считается планированием)
-
если
await_suspendвозвращает void , управление немедленно возвращается вызывающему/возобновителю текущей сопрограммы (эта сопрограмма остаётся приостановленной), иначе -
если
await_suspendвозвращает bool ,
-
- значение true возвращает управление вызывающему/возобновителю текущей сопрограммы
- значение false возобновляет текущую сопрограмму.
-
если
await_suspendвозвращает дескриптор сопрограммы для другой сопрограммы, этот дескриптор возобновляется (путем вызова handle. resume ( ) ) (примечание: это может вызвать цепочку, которая в конечном итоге приведёт к возобновлению текущей сопрограммы). -
если
await_suspendвыбрасывает исключение, исключение перехватывается, сопрограмма возобновляется, и исключение немедленно перебрасывается.
-
если
Наконец, awaiter. await_resume ( ) вызывается (независимо от того, была ли корутина приостановлена), и его результат становится результатом всего выражения co_await expr .
Если сопрограмма была приостановлена в выражении co_await и позже возобновлена, точка возобновления находится непосредственно перед вызовом awaiter. await_resume ( ) .
Обратите внимание, что корутина полностью приостанавливается перед входом в awaiter. await_suspend ( ) . Её дескриптор может быть передан в другой поток и возобновлён до того, как await_suspend ( ) функция завершится. (Учтите, что стандартные правила безопасности памяти остаются в силе, поэтому если дескриптор корутины передаётся между потоками без блокировки, ожидатель должен использовать как минимум релиз-семантику , а возобновляющий — как минимум аквир-семантику .) Например, дескриптор корутины может быть помещён в колбэк, запланированный на выполнение в пуле потоков при завершении асинхронной операции ввода-вывода. В этом случае, поскольку текущая корутина могла быть возобновлена и, следовательно, выполнила деструктор объекта ожидателя, всё это может происходить параллельно с выполнением await_suspend ( ) в текущем потоке. await_suspend ( ) должен рассматривать * this как уничтоженный и не обращаться к нему после публикации дескриптора в другие потоки.
Пример
#include <coroutine> #include <iostream> #include <stdexcept> #include <thread> auto switch_to_new_thread(std::jthread& out) { struct awaitable { std::jthread* p_out; bool await_ready() { return false; } void await_suspend(std::coroutine_handle<> h) { std::jthread& out = *p_out; if (out.joinable()) throw std::runtime_error("Output jthread parameter not empty"); out = std::jthread([h] { h.resume(); }); // Potential undefined behavior: accessing potentially destroyed *this // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK } void await_resume() {} }; return awaitable{&out}; { struct task { struct promise_type { task get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; task resuming_on_new_thread(std::jthread& out) { std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // awaiter destroyed here std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; } int main() { std::jthread out; resuming_on_new_thread(out); }
Возможный вывод:
Coroutine started on thread: 139972277602112 New thread ID: 139972267284224 Coroutine resumed on thread: 139972267284224
Примечание: объект awaiter является частью состояния сопрограммы (как временный объект, время жизни которого пересекает точку приостановки) и уничтожается до завершения выражения co_await . Он может использоваться для поддержания состояния операции, требуемого некоторыми асинхронными I/O API, без прибегания к дополнительным динамическим выделениям памяти.
Стандартная библиотека определяет два тривиальных объекта ожидания: std::suspend_always и std::suspend_never .
|
Этот раздел не завершён
Причина: примеры |
| Демонстрация promise_type :: await_transform и предоставленного программой объекта ожидания |
|---|
Пример
Запустить этот код
#include <cassert> #include <coroutine> #include <iostream> struct tunable_coro { // Ожидатель, «готовность» которого определяется через параметр конструктора. class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // Три стандартные функции интерфейса awaiter: bool await_ready() const noexcept { return ready_; } static void await_suspend(std::coroutine_handle<>) noexcept {} static void await_resume() noexcept {} }; struct promise_type { using coro_handle = std::coroutine_handle<promise_type>; auto get_return_object() { return coro_handle::from_promise(*this); } static auto initial_suspend() { return std::suspend_always(); } static auto final_suspend() noexcept { return std::suspend_always(); } static void return_void() {} static void unhandled_exception() { std::terminate(); } // Пользовательская функция преобразования, возвращающая пользовательский awaiter: auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); } void disable_suspension() { ready_ = false; } private: bool ready_{true}; }; tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); } // Для простоты объявите эти 4 специальные функции как удаленные: tunable_coro(tunable_coro const&) = delete; tunable_coro(tunable_coro&&) = delete; tunable_coro& operator=(tunable_coro const&) = delete; tunable_coro& operator=(tunable_coro&&) = delete; ~tunable_coro() { если (handle_) handle_.destroy(); } void disable_suspension() const { если (handle_.done()) return; handle_.promise().disable_suspension(); handle_(); } bool operator()() { если (!handle_.done()) handle_(); return !handle_.done(); } private: promise_type::coro_handle handle_; }; tunable_coro generate(int n) { for (int i{}; i != n; ++i) { std::cout << i << ' '; // The awaiter passed to co_await goes to promise_type::await_transform which // issues tunable_awaiter, который изначально вызывает приостановку (возврат обратно в // main на каждой итерации), но после вызова disable_suspension приостановка не выполняется // происходит, и цикл выполняется до своего завершения без возврата в main(). co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // испускает только первый элемент равный 0 for (int k{}; k < 4; ++k) { coro(); // выводит 1 2 3 4, по одному на каждой итерации std::cout << ": "; } coro.disable_suspension(); coro(); // выводит конечные числа 5 6 7 все сразу } Вывод: 0 1 : 2 : 3 : 4 : 5 6 7 ` представляет собой числовую последовательность с разделителями, которая не требует перевода, так как: 1. Числа являются универсальными символами 2. Двоеточия используются как синтаксические разделители 3. Согласно условиям, содержимое тегов `` не переводится Исходный HTML-код полностью сохранен без изменений. |
co_yield
co_yield
выражение возвращает значение вызывающей стороне и приостанавливает текущую корутину: это основной строительный блок возобновляемых функций-генераторов.
co_yield
выражение
|
|||||||||
co_yield
список-инициализации-в-фигурных-скобках
|
|||||||||
Это эквивалентно
co_await promise.yield_value(expr)
Типичный генератор
yield_value
сохраняет (копирует/перемещает или просто сохраняет адрес, так как время жизни аргумента пересекает точку приостановки внутри
co_await
) свой аргумент в объект генератора и возвращает
std::suspend_always
, передавая управление вызывающему/возобновляющему коду.
#include <coroutine> #include <cstdint> #include <exception> #include <iostream> template<typename T> struct Generator { // Имя класса 'Generator' является нашим выбором и не требуется для корутины // магия. Компилятор распознает сопрограмму по наличию ключевого слова 'co_yield'. // Вы можете использовать имя 'MyGenerator' (или любое другое имя) вместо этого, при условии что вы включите // вложенная структура promise_type с методом 'MyGenerator get_return_object()'. // (Примечание: Необходимо скорректировать объявления конструкторов и деструкторов // при переименовании.) struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type // обязательный { T value_; std::exception_ptr exception_; Generator get_return_object() { return Generator(handle_type::from_promise(*this)); } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { exception_ = std::current_exception(); } // сохранение // исключение template<std::convertible_to<T> From> // C++20 концепт std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // кэширование результата в promise return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // Единственный надежный способ определить, завершилась ли корутина или нет, // будет ли сгенерировано следующее значение (co_yield) // в сопрограмме через C++ геттер (operator () ниже) предназначен для выполнения/возобновления // корутина до следующей точки co_yield (или до завершения при выходе из функции). // Затем мы сохраняем/кэшируем результат в promise, чтобы позволить геттеру (operator() ниже) // чтобы захватить его без выполнения сопрограммы). return !h_.done(); } T operator()() { fill(); full_ = false; // мы собираемся удалить ранее закэшированные // результат, чтобы сделать promise снова пустым return std::move(h_.promise().value_); } private: bool full_ = false; void fill() { if (!full_) { h_(); if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_); // propagate coroutine exception in called context full_ = true; } } }; Generator<std::uint64_t> fibonacci_sequence(unsigned n) { если (n == 0) co_return; if (n > 94) throw std::runtime_error("Слишком большая последовательность Фибоначчи. Элементы выйдут за пределы диапазона."); co_yield 0; if (n == 1) co_return; co_yield 1; if (n == 2) co_return; std::uint64_t a = 0; std::uint64_t b = 1; for (unsigned i = 2; i < n; ++i) { std::uint64_t s = a + b; co_yield s; a = b; b = s; } } int main() { try { auto gen = fibonacci_sequence(10); // максимум 94 до переполнения uint64_t for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr (Примечание: В данном случае переводить нечего, так как элемент содержит только HTML-разметку и C++ специфичный термин `std::cerr`, который согласно инструкциям не подлежит переводу. Весь текст уже находится внутри HTML-тегов и представляет собой код C++.) << "Исключение: " << ex.что() << '\n'; } catch (...) { std::cerr << "Неизвестное исключение.\n"; } }
Вывод:
fib(0)=0 fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34
Примечания
| Макрос тестирования возможностей | Значение | Стандарт | Возможность |
|---|---|---|---|
__cpp_impl_coroutine
|
201902L
|
(C++20) | Coroutines (поддержка компилятора) |
__cpp_lib_coroutine
|
201902L
|
(C++20) | Coroutines (поддержка библиотеки) |
__cpp_lib_generator
|
202207L
|
(C++23) | std::generator : синхронный генератор корутин для диапазонов |
Ключевые слова
co_await , co_return , co_yield
Поддержка библиотек
Библиотека поддержки корутин определяет несколько типов, предоставляющих поддержку корутин на этапах компиляции и выполнения.
Отчёты о дефектах
Следующие отчеты об изменениях в поведении, содержащие описания дефектов, были применены ретроактивно к ранее опубликованным стандартам C++.
| DR | Applied to | Behavior as published | Correct behavior |
|---|---|---|---|
| CWG 2556 | C++20 |
invalid
return_void
made the behavior of
falling off the end of the coroutine undefined |
the program is ill-
formed in this case |
| CWG 2668 | C++20 | co_await could not appear in lambda expressions | allowed |
| CWG 2754 | C++23 |
*
this
was taken when constructing the promise
object for explicit object member functions |
*
this
is not
taken in this case |
Смотрите также
|
(C++23)
|
view
представляющий синхронный
coroutine
генератор
(шаблон класса) |
Внешние ссылки
| 1. | Льюис Бейкер, 2017-2022 - Asymmetric Transfer. |
| 2. | Дэвид Мазьерс, 2021 - Tutorial on C++20 coroutines. |
| 3. | Сюаньци Сю & Юй Ци & Яо Хань, 2021 - C++20 Principles and Applications of Coroutine. (Chinese) |
| 4. | Саймон Татхэм, 2023 - Writing custom C++20 coroutine systems. |