Master C++20 Coroutines: From Basics to Advanced Examples
This article walks through C++20 coroutines with clear explanations, three runnable examples, deep dives into promise_type, coroutine_handle, co_await, co_yield and co_return, and shows how to build both void‑returning and int‑returning coroutines while illustrating the compiler‑generated control flow.
The article introduces C++20 coroutines by presenting three complete, runnable examples and explains the underlying concepts such as co_await, co_yield, co_return, promise_type, coroutine_handle and the coroutine frame.
What is a C++ coroutine?
From a syntactic point of view, any function that contains co_await, co_yield or co_return is a coroutine. System‑wise, a coroutine is a piece of code running on a thread whose execution can be suspended and resumed, enabling asynchronous behavior without creating additional threads. Execution‑wise, a normal function has two states (invoke → finalize), while a coroutine has four states (invoke → suspend ↔ resume → finalize).
Differences between normal functions and coroutines
In a non‑coroutine, a function always starts at its first line and returns only via return. In a coroutine, the function may suspend multiple times, each co_yield returns a value to the caller, and execution resumes from the point after the previous suspension.
Key questions
Where is the suspended state saved? – In the coroutine frame.
Who creates and destroys the frame? – The compiler inserts code that allocates the frame, constructs the promise_type, and later destroys it.
How is the return value delivered? – The compiler passes it to the promise_type, which stores it; the caller accesses it via the coroutine handle.
First example – a void‑returning coroutine
#include <iostream>
#include <coroutine>
struct future_type {
struct promise_type;
using co_handle_type = std::coroutine_handle<promise_type>;
future_type(co_handle_type h) { std::cout << "future_type constructor" << std::endl; co_handle_ = h; }
~future_type() { std::cout << "future_type destructor" << std::endl; co_handle_.destroy(); }
future_type(const future_type&) = delete;
future_type(future_type&&) = delete;
bool resume() { if (!co_handle_.done()) co_handle_.resume(); return !co_handle_.done(); }
co_handle_type co_handle_;
};
struct future_type::promise_type {
auto get_return_object() { std::cout << "get_return_object" << std::endl; return co_handle_type::from_promise(*this); }
auto initial_suspend() { std::cout << "initial_suspend" << std::endl; return std::suspend_always(); }
auto final_suspend() noexcept { std::cout << "final_suspend" << std::endl; return std::suspend_always(); }
void return_void() { std::cout << "return_void" << std::endl; }
void unhandled_exception() { std::cout << "unhandled_exception" << std::endl; std::terminate(); }
};
future_type three_step_coroutine() {
std::cout << "three_step_coroutine begin" << std::endl;
co_await std::suspend_always();
std::cout << "three_step_coroutine running" << std::endl;
co_await std::suspend_always();
std::cout << "three_step_coroutine end" << std::endl;
}
int main() {
future_type ret = three_step_coroutine();
std::cout << "=======calling first resume======" << std::endl; ret.resume();
std::cout << "=======calling second resume=====" << std::endl; ret.resume();
std::cout << "=======calling third resume======" << std::endl; ret.resume();
std::cout << "=======main end======" << std::endl;
return 0;
}The program prints the construction/destruction messages and shows how each co_await std::suspend_always() suspends the coroutine, returning control to main after each resume().
Second example – an int‑returning coroutine
#include <iostream>
#include <coroutine>
using namespace std;
struct future_type_int {
struct promise_type;
using co_handle_type = coroutine_handle<promise_type>;
future_type_int(co_handle_type h) { cout << "future_type_int constructor" << endl; co_handle_ = h; }
~future_type_int() { cout << "future_type_int destructor" << endl; co_handle_.destroy(); }
future_type_int(const future_type_int&) = delete;
future_type_int(future_type_int&&) = delete;
bool resume() { if (!co_handle_.done()) co_handle_.resume(); return !co_handle_.done(); }
bool await_ready() { return false; }
bool await_suspend(coroutine_handle<>) { resume(); return false; }
auto await_resume() { return co_handle_.promise().ret_val; }
co_handle_type co_handle_;
};
struct future_type_int::promise_type {
int ret_val;
promise_type() { cout << "promise_type constructor" << endl; }
~promise_type() { cout << "promise_type destructor" << endl; }
auto get_return_object() { cout << "get_return_object" << endl; return co_handle_type::from_promise(*this); }
auto initial_suspend() { cout << "initial_suspend" << endl; return suspend_always(); }
auto final_suspend() noexcept { cout << "final_suspend" << endl; return suspend_never(); }
void return_value(int v) { cout << "return_value : " << v << endl; ret_val = v; }
void unhandled_exception() { cout << "unhandled_exception" << endl; terminate(); }
auto yield_value(int v) { cout << "yield_value : " << v << endl; ret_val = v; return suspend_always(); }
};
future_type_int three_step_coroutine() {
cout << "three_step_coroutine begin" << endl;
co_yield 222;
cout << "three_step_coroutine running" << endl;
co_yield 333;
cout << "three_step_coroutine end" << endl;
co_return 444;
}
int main() {
future_type_int f = three_step_coroutine();
cout << "=======calling first resume======" << endl; f.resume(); cout << "ret_val = " << f.co_handle_.promise().ret_val << endl;
cout << "=======calling second resume=====" << endl; f.resume(); cout << "ret_val = " << f.co_handle_.promise().ret_val << endl;
cout << "=======calling third resume======" << endl; f.resume(); cout << "ret_val = " << f.co_handle_.promise().ret_val << endl;
cout << "=======main end======" << endl;
return 0;
}This example demonstrates how co_yield and co_return interact with promise_type to deliver intermediate and final values.
Awaitable and Awaiter
co_awaitworks with an awaitable object. An awaitable must provide await_ready, await_suspend and await_resume. The compiler first checks whether the current promise_type implements await_transform; if not, it uses the three methods of the operand.
The three methods control suspension, resumption and the value returned to the caller. Depending on the return type of await_suspend (void, bool, false, or another coroutine handle) the control flow differs.
Full compiler‑generated skeleton
{
co_await promise.initial_suspend();
try {
<body>
} catch (...) {
promise.unhandled_exception();
}
co_await promise.final_suspend();
}The article concludes with a discussion of the non‑uniform semantics of co_await, the importance of understanding promise_type, coroutine_handle, and the awaiter/awaitable relationship, and provides references for further reading.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
NetEase Smart Enterprise Tech+
Get cutting-edge insights from NetEase's CTO, access the most valuable tech knowledge, and learn NetEase's latest best practices. NetEase Smart Enterprise Tech+ helps you grow from a thinker into a tech expert.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
