Stackless coroutine for C++, but zero-allocation. Rebirth of CO2.
- Boost.Config
- Boost.Preprocessor
C++20 introduced coroutine into the language, however, in many cases (especially in async scenario), it incurs memory allocation, as HALO is not guaranteed. This creates resistance to its usage, as the convenience it offers may not worth the overhead it brings. People will tend to write monolithic coroutines instead of splitting them into small, reusable coroutines in fear of introducing too many allocations, this is contrary to the discipline of programming.
COZ is a single-header library that utilizes preprocessor & compiler magic to emulate the C++ coroutine, while requires zero allocation, it also doesn't require type erasure. With COZ, the entire coroutine is under your control, unlike standard coroutine, which can only be accessed indirectly via the coroutine_handle.
NOTE
COZ uses stateful metaprogramming technique, which may not be blessed by the standard committee.
This library is modeled after the standard coroutine. It offers several macros to replace the language counterparts.
To use it, #include <coz/coroutine.hpp>
A coroutine written in this library looks like below:
auto function(Args... args) COZ_BEG(promise-initializer, (captured-args...),
local-vars...;
) {
// for generator-like coroutine
COZ_YIELD(...);
// for task-like coroutine
COZ_AWAIT(...);
COZ_RETURN(...);
} COZ_ENDThe coroutine body has to be surrounded with 2 macros: COZ_BEG and COZ_END.
The macro COZ_BEG takes some parameters:
- promise-initializer - expression to initialize the promise, e.g.
async<int>(exe) - captured-args (optional) - comma separated args to be captured, e.g.
(a, b) - local-vars (optional) - local-variable definitions, e.g.
int a = 42;
If there's no captured-args and locals, it looks like:
COZ_BEG(init, ())The promise-initializer is an expression, whose type must define a promise_type, which will be constructed with the expression.
It can take args from the function params. For example, you can take an executor to be used for the promise.
template<class Exe>
auto f(Exe exe) COZ_BEG(async<int>(exe), ())- the args (e.g.
exein above example) don't have to be in the captured-args. - if the expression contains comma that is not in parentheses, you must surround the it with parentheses (e.g.
(task<T, E>)).
You can intialize the local variables as below:
auto f(int i) COZ_BEG(init, (i),
int i2 = i * 2; // can refer to the arg
std::string msg{"hello"};
) ...()initializer cannot be used.autodeduced variable cannot be used.
Inside the coroutine body, there are some restrictions:
- local variables with automatic storage cannot cross suspension points - you should specify them in local variables section of
COZ_BEGas described above switchbody cannot contain suspension points.- identifiers starting with
_coz_are reserved for this library - Some language constructs should use their marcro replacements (see below).
After defining the coroutine body, remember to close it with COZ_END.
It has 4 variants: COZ_AWAIT, COZ_AWAIT_SET, COZ_AWAIT_APPLY and COZ_AWAIT_LET.
| MACRO | Core Language |
|---|---|
COZ_AWAIT(expr) |
co_await expr |
COZ_AWAIT_SET(var, expr) |
var = co_await expr |
COZ_AWAIT_APPLY(f, expr, args...) |
f(co_await expr, args...) |
COZ_AWAIT_LET(var-decl, expr) {...} |
{var-decl = co_await expr; ...} |
- The
expris either used directly or transformed.operator co_awaitis not used. finCOZ_AWAIT_APPLYcan also be a marco (e.g.COZ_RETURN)COZ_AWAIT_LETallows you to declare a local variable that binds to theco_awaitresult, then you can process it in the brace scope.
| MACRO | expr Lifetime |
|---|---|
COZ_YIELD(expr) |
transient |
COZ_YIELD_KEEP(expr) |
cross suspension point |
promise.yield_value(expr);
<suspend>- It differs from the standard semantic, which is equivalent to
co_await promise.yield_value(expr). Instead, we ignore the result ofyield_valueand just suspend afterward. - While
COZ_YIELD_KEEPis more general,COZ_YIELDis more optimization-friendly.
| MACRO | Core Language |
|---|---|
COZ_RETURN() |
co_return |
COZ_RETURN(expr) |
co_return expr |
Needed only if the try-block contains suspension points.
COZ_TRY {
...
} COZ_CATCH (const std::runtime_error& e) {
...
} catch (const std::exception& e) {
...
}Only the first catch clause needs to be written as COZ_CATCH, the subsequent ones should use the plain catch.
coz::coroutine has interface defined as below:
template<class Promise, class Params, class State>
struct coroutine {
template<class Init>
explicit coroutine(Init&& init);
// No copy.
coroutine(const coroutine&) = delete;
coroutine& operator=(const coroutine&) = delete;
coroutine_handle<Promise> handle() noexcept;
Promise& promise() noexcept;
const Promise& promise() const noexcept;
bool done() const noexcept;
void start(Params&& params);
void resume();
void destroy();
};- The
initconstructor param is the promise-initializer. - The lifetime of
Promiseis tied to the coroutine. - Non-started coroutine is considered to be
done. - Don't call
destroyif it's alreadydone.
coz::coroutine_handle has the same interface as the standard one.
This defines what is returned from the coroutine. The prototype is:
template<class Init, class Params, class State>
struct co_result;The first template param (i.e. Init) is the type of promise-initializer.
Params and State are the template params that you should pass to coz::coroutine<Promise, Params, State>, the Promise should be the same as Init::promise_type.
Users could customize it like below:
template<class Params, class State>
struct [[nodiscard]] coz::co_result<MyCoroInit, Params, State> {
MyCoroInit m_init;
Params m_params;
// optional
auto get_return_object();
...
};co_resultwill be constructed the with the promise-initializer and the captured-args.- if
get_return_objectis defined, its result is returned; otherwise, theco_resultitself is returned.
The interface for Promise looks like below:
struct Promise {
void finalize();
// either
void return_void();
// or
void return_value();
void unhandled_exception();
// optional
auto await_transform(auto expr);
};- There's no
initial_suspendandfinal_suspend. The user should callcoroutine::startto start the coroutine. - Once the coroutine stops (either normally or via
destroy) thePromise::finalizewill be called. await_transformis not greedy (i.e. could be filtered by SFINAE).
The interface for Awaiter looks like below:
struct Awaiter {
bool await_ready();
// either
void await_suspend(coroutine_handle<Promise> coro);
// or
bool await_suspend(coroutine_handle<Promise> coro);
T await_resume();
};- Unlike standard coroutine,
await_suspendcannot returncoroutine_handle.
Copyright (c) 2024-2025 Jamboree
Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)