Coroutines (C++20)
Una corrutina es una función que puede suspender su ejecución para ser reanudada posteriormente. Las corrutinas son sin pila: suspenden la ejecución al retornar al llamador, y los datos requeridos para reanudar la ejecución se almacenan separadamente de la pila. Esto permite código secuencial que se ejecuta de forma asíncrona (por ejemplo, para manejar E/S no bloqueante sin callbacks explícitos), y también soporta algoritmos sobre secuencias infinitas de cálculo diferido y otros usos.
Una función es una corrutina si su definición contiene cualquiera de los siguientes elementos:
- la expresión co_await — para suspender la ejecución hasta que se reanude
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)); } }
- la expresión co_yield — para suspender la ejecución devolviendo un valor
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- la co_return statement — para completar la ejecución devolviendo un valor
lazy<int> f() { co_return 7; }
Cada corrutina debe tener un tipo de retorno que cumpla con una serie de requisitos, señalados a continuación.
Contenidos |
Restricciones
Las corrutinas no pueden usar
argumentos variádicos
, sentencias
return
simples, o
tipos de retorno de marcador de posición
(
auto
o
Concept
).
Funciones consteval , funciones constexpr , constructores , destructores , y la función main no pueden ser corrutinas.
Ejecución
Cada corrutina está asociada con
- el objeto promise , manipulado desde dentro de la corrutina. La corrutina envía su resultado o excepción a través de este objeto. Los objetos promise no guardan relación alguna con std::promise .
- el coroutine handle , manipulado desde fuera de la corrutina. Este es un manejador no propietario utilizado para reanudar la ejecución de la corrutina o para destruir el frame de la corrutina.
- el coroutine state , que es un almacenamiento interno, asignado dinámicamente (a menos que la asignación sea optimizada), objeto que contiene
-
- el objeto promise
- los parámetros (todos copiados por valor)
- alguna representación del punto de suspensión actual, para que un resume sepa dónde continuar, y un destroy sepa qué variables locales estaban en alcance
- variables locales y temporales cuya duración abarca el punto de suspensión actual.
Cuando una corrutina comienza su ejecución, realiza lo siguiente:
- asigna el objeto de estado de la corrutina usando operator new .
- copia todos los parámetros de la función al estado de la corrutina: los parámetros por valor se mueven o copian, los parámetros por referencia permanecen como referencias (por lo tanto, pueden quedar colgadas si la corrutina se reanuda después de que finalice la vida útil del objeto referido — ver ejemplos más adelante).
- llama al constructor para el objeto promise. Si el tipo promise tiene un constructor que toma todos los parámetros de la corrutina, se llama a ese constructor, con los argumentos de la corrutina posteriores a la copia. De lo contrario, se llama al constructor por defecto.
- llama a promise. get_return_object ( ) y guarda el resultado en una variable local. El resultado de esa llamada se devolverá al llamador cuando la corrutina se suspenda por primera vez. Cualquier excepción lanzada hasta este paso inclusive se propaga de vuelta al llamador, no se coloca en el promise.
-
llama a
promise.
initial_suspend
(
)
y
co_awaitsu resultado. Los tiposPromisetípicos devuelven std::suspend_always , para corrutinas de inicio diferido, o std::suspend_never , para corrutinas de inicio inmediato. - cuando co_await promise. initial_suspend ( ) se reanuda, comienza a ejecutar el cuerpo de la corrutina.
Algunos ejemplos de un parámetro que se convierte en colgante:
#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} destruido h.resume(); // la corrutina reanudada ejecuta std::cout << i, usa S::i después de liberar h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // la corrutina devuelta no puede reanudarse sin cometer uso después de liberar } void bad3() { coroutine h = [i = 0]() -> coroutine // una lambda que también es una corrutina { std::cout << i; co_return; }(); // invocada inmediatamente // lambda destruida h.resume(); // usa (tipo lambda anónimo)::i después de liberar h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // hacer i un parámetro de la corrutina { std::cout << i; co_return; }(0); // lambda destruida h.resume(); // sin problema, i ha sido copiado al marco de la corrutina // como un parámetro por valor h.destroy(); }
Cuando una corrutina alcanza un punto de suspensión
- el objeto de retorno obtenido anteriormente se devuelve al llamador/resumidor, después de la conversión implícita al tipo de retorno de la corrutina, si es necesario.
Cuando una corrutina alcanza la declaración co_return , realiza lo siguiente:
- llama a promise. return_void ( ) para
-
- co_return ;
- co_return expr ; donde expr tiene tipo void
- o llama promise. return_value ( expr ) para co_return expr ; donde expr tiene tipo no void
- destruye todas las variables con duración de almacenamiento automático en orden inverso al que fueron creadas.
- llama promise. final_suspend ( ) y co_await el resultado.
Caer al final de la corrutina es equivalente a
co_return
;
, excepto que el comportamiento es indefinido si no se pueden encontrar declaraciones de
return_void
en el ámbito de
Promise
. Una función sin ninguna de las palabras clave definitorias en su cuerpo de función no es una corrutina, independientemente de su tipo de retorno, y caer al final resulta en comportamiento indefinido si el tipo de retorno no es (posiblemente calificado cv)
void
.
// suponiendo que task es algún tipo de tarea de corrutina task<void> f() { // no es una corrutina, comportamiento indefinido } task<void> g() { co_return; // OK } task<void> h() { co_await g(); // OK, co_return implícito; }
Si la corrutina termina con una excepción no capturada, realiza lo siguiente:
- captura la excepción y llama promise. unhandled_exception ( ) desde dentro del bloque catch
- llama promise. final_suspend ( ) y co_await al resultado (por ejemplo, para reanudar una continuación o publicar un resultado). Es comportamiento indefinido reanudar una corrutina desde este punto.
Cuando el estado de la corrutina es destruido, ya sea porque terminó mediante co_return o por una excepción no capturada, o porque fue destruido mediante su manejador, realiza lo siguiente:
- llama al destructor del objeto promise.
- llama a los destructores de las copias de los parámetros de la función.
- llama a operator delete para liberar la memoria utilizada por el estado de la corrutina.
- transfiere la ejecución de vuelta al llamador/resumidor.
Asignación dinámica
El estado de la corrutina se asigna dinámicamente mediante el operator new no-array.
Si el tipo
Promise
define un reemplazo a nivel de clase, será utilizado; de lo contrario, se utilizará el
operator new
global.
Si el tipo
Promise
define una forma de colocación de
operator new
que toma parámetros adicionales, y estos coinciden con una lista de argumentos donde el primer argumento es el tamaño solicitado (de tipo
std::size_t
) y el resto son los argumentos de la función de corrutina, esos argumentos se pasarán a
operator new
(esto hace posible usar la
convención de asignador principal
para corrutinas).
La llamada a operator new puede ser optimizada (incluso si se utiliza un asignador personalizado) si
- La duración del estado de la corrutina está estrictamente anidada dentro de la duración del llamador, y
- el tamaño del marco de corrutina se conoce en el sitio de llamada.
En ese caso, el estado de la corrutina está incrustado en el marco de pila del llamador (si el llamador es una función ordinaria) o estado de corrutina (si el llamador es una corrutina).
Si la asignación falla, la corrutina lanza
std::bad_alloc
, a menos que el tipo
Promise
defina la función miembro
Promise
::
get_return_object_on_allocation_failure
(
)
. Si esa función miembro está definida, la asignación utiliza la forma nothrow de
operator new
y en caso de fallo de asignación, la corrutina retorna inmediatamente el objeto obtenido de
Promise
::
get_return_object_on_allocation_failure
(
)
al llamador, por ejemplo:
struct Coroutine::promise_type { /* ... */ // asegurar el uso de operator-new que no lanza excepciones static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // o, return Coroutine(nullptr); } // sobrecarga personalizada de new que no lanza excepciones void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // fallo de asignación } };
Promesa
El tipo
Promise
es determinado por el compilador a partir del tipo de retorno de la corrutina utilizando
std::coroutine_traits
.
Formalmente, sea
-
RyArgs...denotan el tipo de retorno y la lista de tipos de parámetros de una corrutina respectivamente, -
ClassTdenota el tipo de clase a la que pertenece la corrutina si está definida como una función miembro no estática, - cv denota la calificación cv declarada en la declaración de función si está definida como una función miembro no estática,
su
Promise
tipo está determinado por:
- std:: coroutine_traits < R, Args... > :: promise_type , si la corrutina no está definida como una función miembro de objeto implícito ,
-
std::
coroutine_traits
<
R,
cvClassT & , Args... > :: promise_type , si la corrutina está definida como una función miembro de objeto implícito que no está calificada como referencia a valor, -
std::
coroutine_traits
<
R,
cvClassT && , Args... > :: promise_type , si la corrutina está definida como una función miembro de objeto implícito que está calificada como referencia a valor.
Por ejemplo:
| Si la corrutina se define como ... |
entonces su tipo
Promise
es ...
|
|---|---|
| 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
El operador unario co_await suspende una corrutina y devuelve el control al llamador.
co_await
expr
|
|||||||||
Una expresión co_await solo puede aparecer en una expresión potencialmente evaluada dentro del cuerpo de una función regular cuerpo de función (incluyendo el cuerpo de función de una expresión lambda ), y no puede aparecer
- en un manejador ,
- en una sentencia de declaración , a menos que aparezca en un inicializador de dicha sentencia de declaración,
-
en la
declaración simple
de un
init-statement
(ver
if,switch,fory [[../range- for |range- for ]]), a menos que aparezca en un inicializador de dicho init-statement , - en un argumento por defecto , o
- en el inicializador de una variable de ámbito de bloque con duración de almacenamiento estática o de hilo.
|
Una expresión co_await no puede ser una subexpresión potencialmente evaluada del predicado de una aserción de contrato . |
(since C++26) |
Primero, expr se convierte en un awaitable de la siguiente manera:
- si expr es producido por un punto de suspensión inicial, un punto de suspensión final, o una expresión yield, el awaitable es expr , tal cual.
-
de lo contrario, si el tipo
Promisede la corrutina actual tiene la función miembroawait_transform, entonces el awaitable es promise. await_transform ( expr ) . - de lo contrario, el awaitable es expr , tal cual.
Luego, se obtiene el objeto awaiter, de la siguiente manera:
- si la resolución de sobrecarga para operator co_await da una única mejor sobrecarga, el awaiter es el resultado de esa llamada:
-
- awaitable. operator co_await ( ) para la sobrecarga de miembro,
- operator co_await ( static_cast < Awaitable && > ( awaitable ) ) para la sobrecarga no miembro.
- de lo contrario, si la resolución de sobrecarga no encuentra ningún operador co_await , el awaiter es directamente esperable.
- de lo contrario, si la resolución de sobrecarga es ambigua, el programa está mal formado.
Si la expresión anterior es un prvalue , el objeto awaiter es un temporal materializado a partir de él. De lo contrario, si la expresión anterior es un glvalue , el objeto awaiter es el objeto al cual se refiere.
Entonces, awaiter. await_ready ( ) es llamado (esto es un atajo para evitar el costo de suspensión si se sabe que el resultado está listo o puede completarse sincrónicamente). Si su resultado, convertido contextualmente a bool es false entonces
- La corrutina se suspende (su estado de corrutina se completa con variables locales y el punto de suspensión actual).
-
awaiter.
await_suspend
(
handle
)
se llama, donde handle es el identificador de corrutina que representa la corrutina actual. Dentro de esa función, el estado suspendido de la corrutina es observable a través de ese identificador, y es responsabilidad de esta función programarlo para reanudar en algún ejecutor, o para ser destruido (devolver false cuenta como programación)
-
si
await_suspenddevuelve void , el control se devuelve inmediatamente al llamador/reanudador de la corrutina actual (esta corrutina permanece suspendida), de lo contrario -
si
await_suspenddevuelve bool ,
-
- el valor true devuelve el control al llamador/reanudador de la corrutina actual
- el valor false reanuda la corrutina actual.
-
si
await_suspenddevuelve un identificador de corrutina para alguna otra corrutina, ese identificador se reanuda (mediante una llamada a handle. resume ( ) ) (nota: esto puede encadenarse para eventualmente causar que la corrutina actual se reanude). -
si
await_suspendlanza una excepción, la excepción se captura, la corrutina se reanuda y la excepción se vuelve a lanzar inmediatamente.
-
si
Finalmente, awaiter. await_resume ( ) es llamado (ya sea que la corrutina haya sido suspendida o no), y su resultado es el resultado de toda la expresión co_await expr .
Si la corrutina fue suspendida en la expresión co_await , y posteriormente es reanudada, el punto de reanudación es inmediatamente antes de la llamada a awaiter. await_resume ( ) .
Tenga en cuenta que la corrutina está completamente suspendida antes de entrar en awaiter. await_suspend ( ) . Su manejador puede compartirse con otro hilo y reanudarse antes de que la función await_suspend ( ) retorne. (Nótese que las reglas predeterminadas de seguridad de memoria aún se aplican, por lo que si un manejador de corrutina se comparte entre hilos sin un bloqueo, el awaiter debe usar al menos semánticas de liberación y el reanudador debe usar al menos semánticas de adquisición .) Por ejemplo, el manejador de corrutina puede colocarse dentro de una devolución de llamada, programada para ejecutarse en un grupo de hilos cuando se complete la operación de E/S asíncrona. En ese caso, dado que la corrutina actual pudo haber sido reanudada y por lo tanto ejecutado el destructor del objeto awaiter, todo concurrentemente mientras await_suspend ( ) continúa su ejecución en el hilo actual, await_suspend ( ) debe tratar * this como destruido y no acceder a él después de que el manejador fue publicado a otros hilos.
Ejemplo
#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); }
Salida posible:
Coroutine started on thread: 139972277602112 New thread ID: 139972267284224 Coroutine resumed on thread: 139972267284224
Nota: el objeto awaiter es parte del estado de la corrutina (como un temporal cuya vida cruza un punto de suspensión) y se destruye antes de que la expresión co_await finalice. Puede utilizarse para mantener el estado por operación según lo requieren algunas APIs de E/S asíncronas sin recurrir a asignaciones dinámicas adicionales.
La biblioteca estándar define dos awaitables triviales: std::suspend_always y std::suspend_never .
|
Esta sección está incompleta
Razón: ejemplos |
| Demostración de promise_type :: await_transform y un awaiter proporcionado por el programa |
|---|
Ejemplo
Ejecutar este código
#include <cassert> #include <coroutine> #include <iostream> struct tunable_coro { // Un awaiter cuya "disponibilidad" se determina mediante el parámetro del constructor. class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // Tres funciones estándar de la interfaz 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(); } // Una función de transformación proporcionada por el usuario que devuelve el awaiter personalizado: 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); } // Por simplicidad, declare estas 4 funciones especiales como eliminadas: 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() { if (handle_) handle_.destroy(); } void disable_suspension() const { if (handle_.done()) return; handle_.promise().disable_suspension(); handle_(); } bool operator()() { if (!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 que inicialmente causa suspensión (regresando a // main en cada iteración), pero después de una llamada a disable_suspension no hay suspensión // ocurre y el bucle se ejecuta hasta su finalización sin retornar a main(). co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // emite solo el primer elemento == 0 for (int k{}; k < 4; ++k) { coro(); // emite 1 2 3 4, uno por cada iteración std::cout << ": "; } coro.disable_suspension(); coro(); // emite los números de cola 5 6 7 todos a la vez } Salida: 0 1 : 2 : 3 : 4 : 5 6 7 |
co_yield
co_yield
expression devuelve un valor al llamador y suspende la corrutina actual: es el bloque de construcción común de las funciones generadoras reanudables.
co_yield
expr
|
|||||||||
co_yield
braced-init-list
|
|||||||||
Es equivalente a
co_await promise.yield_value(expr)
Un generador típico
yield_value
almacenaría (copiar/mover o simplemente almacenar la dirección de, ya que el tiempo de vida del argumento cruza el punto de suspensión dentro del
co_await
) su argumento en el objeto generador y retornaría
std::suspend_always
, transfiriendo control al llamador/resumidor.
#include <coroutine> #include <cstdint> #include <exception> #include <iostream> template<typename T> struct Generator { // El nombre de la clase 'Generator' es nuestra elección y no es requerido para coroutine // magia. El compilador reconoce la corrutina por la presencia de la palabra clave 'co_yield'. // Puedes usar el nombre 'MyGenerator' (o cualquier otro nombre) en su lugar siempre que incluyas // estructura anidada promise_type con el método 'MyGenerator get_return_object()'. // (Nota: Es necesario ajustar las declaraciones de constructores y destructores // cuando se renombrar.) struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type // requerido { 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(); } // guardando // exception template<std::convertible_to<T> From> // C++20 concept std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // almacenando en caché el resultado en promise return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // La única manera confiable de averiguar si hemos finalizado la corrutina o no, // si habrá o no un siguiente valor generado (co_yield) // en corrutina mediante getter de C++ (operator () abajo) es para ejecutar/reanudar // coroutine hasta el siguiente punto co_yield (o dejarla terminar al final). // Luego almacenamos/en cache el resultado en la promesa para permitir el getter (operator() a continuación // para capturarlo sin ejecutar la corrutina). return !h_.done(); } T operator()() { fill(); full_ = false; // vamos a mover nuestro previamente almacenado en caché // resultado para hacer la promesa vacía de nuevo return std::mover(h_.promise().value_); } private: bool full_ = false; void fill() { if (!full_) { h_(); if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_); // propagar excepción de corrutina en contexto llamado full_ = true; } } }; Generator<std::uint64_t> fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw std::runtime_error("Secuencia de Fibonacci demasiado grande. Los elementos desbordarían."); 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); // máximo 94 antes de que uint64_t desborde for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "Excepción: " << ex.qué() << '\n'; } catch (...) { std::cerr << "Excepción desconocida.\n"; } }
Salida:
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
Notas
| Macro de prueba de características | Valor | Estándar | Característica |
|---|---|---|---|
__cpp_impl_coroutine
|
201902L
|
(C++20) | Corrutinas (soporte del compilador) |
__cpp_lib_coroutine
|
201902L
|
(C++20) | Corrutinas (soporte de biblioteca) |
__cpp_lib_generator
|
202207L
|
(C++23) | std::generator : generador de corrutina síncrona para rangos |
Palabras clave
co_await , co_return , co_yield
Soporte de biblioteca
Biblioteca de soporte para corrutinas define varios tipos que proporcionan soporte en tiempo de compilación y ejecución para corrutinas.
Informes de defectos
Los siguientes informes de defectos que modifican el comportamiento se aplicaron retroactivamente a los estándares publicados anteriormente de C++.
| DR | Aplicado a | Comportamiento publicado | Comportamiento correcto |
|---|---|---|---|
| CWG 2556 | C++20 |
un
return_void
inválido hacía que el comportamiento
de caer al final de la corrutina fuera indefinido |
el programa está mal
formado en este caso |
| CWG 2668 | C++20 | co_await no podía aparecer en expresiones lambda | permitido |
| CWG 2754 | C++23 |
*
this
se tomaba al construir el objeto promise
para funciones miembro de objeto explícito |
*
this
no se
toma en este caso |
Véase también
|
(C++23)
|
Un
view
que representa un generador síncrono de
corrutina
(plantilla de clase) |
Enlaces externos
| 1. | Lewis Baker, 2017-2022 - Asymmetric Transfer. |
| 2. | David Mazières, 2021 - Tutorial on C++20 coroutines. |
| 3. | Chuanqi Xu & Yu Qi & Yao Han, 2021 - C++20 Principles and Applications of Coroutine. (Chino) |
| 4. | Simon Tatham, 2023 - Writing custom C++20 coroutine systems. |