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_END
The 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.
exe
in 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.auto
deduced 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_BEG
as described above switch
body 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
expr
is either used directly or transformed.operator co_await
is not used. - If your compiler supports Statement Expression extension (e.g. GCC & Clang), you can use
COZ_AWAIT
as an expression. However, don't use more than oneCOZ_AWAIT
in a single statement, and don't use it as an argument of a function in company with other arguments. f
inCOZ_AWAIT_APPLY
can also be a marco (e.g.COZ_RETURN
)COZ_AWAIT_LET
allows you to declare a local variable that binds to theco_await
result, 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_value
and just suspend afterward. - While
COZ_YIELD_KEEP
is more general,COZ_YIELD
is 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
init
constructor param is the promise-initializer. - The lifetime of
Promise
is tied to the coroutine. - Non-started coroutine is considered to be
done
. - Don't call
destroy
if 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_result
will be constructed the with the promise-initializer and the captured-args.- if
get_return_object
is defined, its result is returned; otherwise, theco_result
itself 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_suspend
andfinal_suspend
. The user should callcoroutine::start
to start the coroutine. - Once the coroutine stops (either normally or via
destroy
) thePromise::finalize
will be called. await_transform
is 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_suspend
cannot returncoroutine_handle
.
Copyright (c) 2024 Jamboree
Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)