C++20 Coroutines
libxpp supports three ways to express async flows — pick the one that fits your compiler:
| Style | Compiler | Blocking? |
|---|---|---|
.then() chains | C++11 | No (callback-driven) |
.await() + fiber | C++11 + XPP_FIBER | No (stackful suspend) |
co_await / co_return | C++20 | No (compiler-generated state machine) |
When to Use co_await
You have multi-step async flows and want linear code. .await() + fiber already gives you this on C++11 — co_await is the C++20 sugar on top.
.await() + fiber (C++11)
int result = xpp::fiber([]() {
int x = xpp::resolve(1).await();
x = x + 1;
x = xpp::work([x] { return x * 2; }).await();
return x - 3;
}).await();
co_await / co_return (C++20)
## Requirements
C++20 compiler with coroutine support. Guarded by `XPP_HAS_COROUTINES` (defined in `<xpp/compiler.h>`). C++17 code is unaffected.
## Promise\<T\> as Coroutine Return Type
Any function returning `Promise<T>` can be a coroutine. Use `co_return` to produce the result:
```cpp
Promise<int> fetch_value() {
co_return 42;
}
Promise<void> do_something() {
co_return; // void coroutine
}
The coroutine is lazy — it doesn't start until wait() (or .then()) drives it via poll().
co_await Promise<U>
co_await works on any Promise<U> (rvalue). The coroutine suspends until the promise resolves, then the co_await expression yields the value:
Promise<int> fetch_and_parse() {
auto data = co_await Promise<std::string>::work(fetch_url);
auto result = co_await work([&] { return parse(data); });
co_return result;
}
You can co_await any Promise source:
Promise::resolve(v)— immediateafter(ms)— timerwork(fn)— thread poolasync<T>()— deferred (pass resolver to another thread)all(...)/race(...)— combinators- Another coroutine's return value
co_await void
co_await Promise<void> suspends and resumes with no value:
Promise<int> delayed_compute() {
co_await after(100); // wait 100ms
co_return 42;
}
Nested Coroutines
Coroutines can co_await other coroutines:
Promise<int> inner() {
co_await after(10);
co_return 100;
}
Promise<int> outer() {
int x = co_await inner();
co_return x + 1;
}
// outer().await() == 101
co_await Combinators
Promise<int> fetch_both() {
auto [status, body] = co_await xpp::all(
work(fetch_status),
Promise<std::string>::work(fetch_body)
);
co_return status + static_cast<int>(body.size());
}
Promise<int> fetch_with_timeout() {
int result = co_await xpp::race(
work(fetch),
after(5000).then([] { return -1; })
);
co_return result;
}
Coroutine + then Chain
Coroutines produce regular Promise<T>, so they compose with .then():
Promise<int> compute() { co_return 10; }
int result = compute().then([](int x) { return x * 3; }).await();
// result == 30
Early Destruction
If a coroutine's Promise<T> is destroyed before completion (e.g., a losing branch in race()), the coroutine frame is safely destroyed. The CoroutinePromiseNode destructor calls handle.destroy(), and any awaited promise's node is released.
{
auto p = slow_coro(); // coroutine that takes 10s
// p destroyed here — coroutine frame destroyed, no crash
}
How It Works
┌──────────────────────────────────────────────────────────────┐
│ CoroutinePromiseNode<T> : public PromiseNode<T> │
│ │
│ poll(waker): │
│ while (true): │
│ if done → return Some(result) │
│ if has await_state: │
│ poll(awaited_promise, waker) │
│ not ready → return None │
│ ready → clear await_state, resume coroutine │
│ else: │
│ resume coroutine (start or continue) │
│ (coroutine may co_return or co_await again) │
│ │
│ coroutine frame: │
│ co_await promise │
│ → PromiseAwaiter::await_suspend(handle) │
│ → extract node, store in CoroutinePromiseNode │
│ → suspend (return to poll) │
│ → poll() polls the node on next call │
│ → when ready, resume coroutine, await_resume() → value │
└──────────────────────────────────────────────────────────────┘
Key: while-loop in poll()
After handle.resume(), the coroutine may have co_awaited an already-resolved promise (e.g., Promise::resolve). The while loop immediately polls it and resumes — no None returned, no busy-loop in wait().
Type-erased await
A Promise<int> coroutine can co_await Promise<string>. The awaited node is stored via type-erased AwaitState:
AwaitStateImpl<U>— pollsPromiseNode<U>, stores result inOption<U>*VoidAwaitState— pollsPromiseNode<void>, setsbool*(becausePromiseNode<void>≠PromiseNode<Void>)
std::coroutine_traits
namespace std {
template <class T>
struct coroutine_traits<xpp::Promise<T>> {
using promise_type = xpp::_::CoroutinePromise<T>;
};
}
No Task<T> wrapper — Promise<T> IS the coroutine return type.