Understanding C++20 Coroutines: Promise, Await, and Practical Examples
C++20 coroutines turn functions containing co_await, co_yield, or co_return into suspendable tasks, requiring a promise_type that defines get_return_object, initial_suspend, final_suspend, yield_value, return handling, and exception management, while handles manage resumption, and custom awaiters enable asynchronous I/O such as non‑blocking TCP connections.
Coroutines are functions that can be suspended and later resumed. In C++20, a function becomes a coroutine when it contains any of co_await , co_yield , or co_return .
A minimal C++20 coroutine example:
coro_ret
number_generator(int begin, int count) {
std::cout << "number_generator invoked." << std::endl;
for (int i = begin; i < count; ++i) {
co_yield i;
}
co_return;
}
int main(int argc, char* argv[]) {
auto g = number_generator(1, 10);
std::cout << "begin to run!" << std::endl;
while (!g.resume()) {
std::cout << "got number:" << g.get() << std::endl;
}
std::cout << "coroutine done, return value:" << g.get() << std::endl;
return 0;
}The function number_generator contains co_yield and co_return , so it is a coroutine. When execution reaches co_yield i , the coroutine suspends; control returns to the caller until resume() is invoked.
The return type of a coroutine must provide a nested promise_type . The promise_type defines several required interfaces:
get_return_object() – creates the coroutine’s return object.
initial_suspend() – decides whether the coroutine starts suspended ( std::suspend_always ) or runs immediately ( std::suspend_never ).
final_suspend() – runs when the coroutine finishes; returning std::suspend_always requires the caller to destroy the coroutine handle.
yield_value(T) – called on each co_yield , stores the yielded value and usually returns std::suspend_always .
return_void() or return_value(T) – invoked by co_return .
unhandled_exception() – handles exceptions thrown inside the coroutine.
A simplified promise_type implementation used in the article:
template
struct coro_ret {
struct promise_type {
std::cout << "promise constructor invoked." << std::endl;
auto get_return_object() {
std::cout << "get_return_object invoked." << std::endl;
return coro_ret{std::coroutine_handle
::from_promise(*this)};
}
auto initial_suspend() {
std::cout << "initial_suspend invoked." << std::endl;
return std::suspend_always{};
}
void return_void() {
std::cout << "return void invoked." << std::endl;
}
auto yield_value(const T& v) {
std::cout << "yield_value invoked." << std::endl;
return_data_ = v;
return std::suspend_always{};
}
auto final_suspend() noexcept {
std::cout << "final_suspend invoked." << std::endl;
return std::suspend_always{};
}
void unhandled_exception() {
std::cout << "unhandled_exception invoked." << std::endl;
std::exit(1);
}
T return_data_;
};
using handle_type = std::coroutine_handle
;
handle_type coro_handle_;
bool resume() {
if (!coro_handle_.done()) coro_handle_.resume();
return coro_handle_.done();
}
T get() const { return coro_handle_.promise().return_data_; }
~coro_ret() { if (coro_handle_) coro_handle_.destroy(); }
coro_ret(handle_type h) : coro_handle_(h) {}
};The article also explains the relationship between three key objects: the promise , the coroutine handle , and the coroutine state . The handle ( std::coroutine_handle<promise_type> ) provides resume() , done() , and destroy() operations, while the promise mediates data exchange between the coroutine and its caller.
For asynchronous I/O, the article shows how to implement a custom awaiter for a non‑blocking connect operation:
struct connect_awaiter {
coroutine_tcp_client& tcp_client_;
bool await_ready() {
switch (tcp_client_.status()) {
case ERROR: std::printf("await_ready: error, no suspend\n"); return true;
case CONNECTED: std::printf("await_ready: already connected, no suspend\n"); return true;
default: std::printf("await_ready: status %d, suspend\n", tcp_client_.status()); return false;
}
}
bool await_suspend(std::coroutine_handle<> awaiting) {
std::printf("await_suspend invoked.\n");
tcp_client_.handle_ = awaiting;
return true; // suspend
}
int await_resume() {
int ret = tcp_client_.status() == CONNECTED ? 0 : -1;
std::printf("await_resume invoked, ret:%d\n", ret);
return ret;
}
};Using this awaiter, a coroutine can perform a TCP connection like:
coro_ret
connect_addr_example(io_service& service, const char* ip, int16_t port) {
coroutine_tcp_client client;
auto connect_ret = co_await client.connect(ip, port, 3, service);
printf("client.connect return:%d\n", connect_ret);
if (connect_ret) {
printf("connect failed, coroutine return\n");
co_return -1;
}
do_something_with_connect(client);
co_return 0;
}The article concludes that C++20 provides a powerful, highly customizable coroutine mechanism, but the standard library support is still limited. Full library support is expected in C++23.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
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.