Coroutines (C++20) — co_await, co_yield, co_return, Generators, Awaitable Types
In this tutorial, you will learn about Coroutines (C++20). We cover key concepts, practical examples, and best practices to help you master this topic.
C++20 coroutines are stackless functions that can suspend execution and resume later, using co_await for asynchronous operations, co_yield for generator sequences, and co_return for returning values.
What You'll Learn
You will write generator coroutines with co_yield to produce lazy sequences, use co_await to suspend execution on asynchronous operations, understand the Coroutine frame, promise object, and awaitable type, implement simple awaiters and promise types, and recognize the tradeoffs of stackless coroutines vs threads.
Why It Matters
Coroutines enable async code that reads like synchronous code, eliminating callback chains. They are the foundation of C++20 asynchronous programming, used in networking libraries, game engines, and UI frameworks. C++ coroutines are zero-overhead when suspended and support complex control flow (loops, try/catch) that callback-based APIs cannot match.
Learning Path
graph LR
A["56: Fold Expressions"] --> B["57: Coroutines"]
B --> C["58: Modules"]
C --> D["59: Exception Safety"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Generator Coroutine with co_yield
The simplest coroutine: a generator that produces a sequence of values lazily.
#include <iostream>
#include <generator>
#include <ranges>
// C++23 generator (simplified — real implementation needs a promise type)
// For C++20, we use a minimal generator:
template <typename T>
struct Generator {
struct promise_type {
T current_value_;
bool finished_ = false;
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
void return_void() { finished_ = true; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(T value) {
current_value_ = std::move(value);
return {};
}
};
std::coroutine_handle<promise_type> handle_;
explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Generator() { if (handle_) handle_.destroy(); }
// Move-only
Generator(Generator&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
Generator& operator=(Generator&& other) noexcept {
if (this != &other) {
if (handle_) handle_.destroy();
handle_ = std::exchange(other.handle_, {});
}
return *this;
}
bool next() {
if (!handle_ || handle_.done()) return false;
handle_.resume();
return !handle_.done();
}
T value() const { return handle_.promise().current_value_; }
// Range support
struct End {};
struct Iterator {
Generator* gen_;
bool operator!=(End) const { return gen_ && !gen_->handle_.done(); }
void operator++() { gen_->next(); }
T operator*() const { return gen_->value(); }
};
Iterator begin() {
if (handle_) handle_.resume();
return Iterator{this};
}
End end() { return {}; }
};
// Generator coroutine
Generator<int> fibonacci(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a; // Suspend, return 'a' to caller
int next = a + b;
a = b;
b = next;
}
}
int main() {
// Use as a range
for (int val : fibonacci(10)) {
std::cout << val << " ";
}
std::cout << "\n"; // 0 1 1 2 3 5 8 13 21 34
// Manual iteration
auto fib = fibonacci(5);
while (fib.next()) {
std::cout << fib.value() << " ";
}
std::cout << "\n"; // 0 1 1 2 3
}
co_await — Suspending Execution
co_await suspends the current coroutine until the awaitable completes.
#include <iostream>
#include <chrono>
#include <thread>
#include <coroutine>
// Simple awaitable: suspend for a duration
struct SleepAwaitable {
std::chrono::milliseconds duration_;
// "await_ready": should we suspend or proceed immediately?
bool await_ready() const noexcept { return duration_.count() == 0; }
// "await_suspend": what happens when we suspend?
void await_suspend(std::coroutine_handle<> handle) const {
auto start = std::chrono::steady_clock::now();
std::thread([handle, start, duration = duration_]() {
std::this_thread::sleep_for(duration);
// Resume the coroutine on the thread
handle.resume();
}).detach();
}
// "await_resume": what value does co_await return?
void await_resume() const noexcept {}
};
// Simple async task
struct Task {
struct promise_type {
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle_;
~Task() { if (handle_) handle_.destroy(); }
};
Task asyncExample() {
std::cout << "Starting async operation...\n";
co_await SleepAwaitable{std::chrono::milliseconds(500)};
std::cout << "After 500ms delay\n";
co_await SleepAwaitable{std::chrono::milliseconds(300)};
std::cout << "After another 300ms\n";
}
int main() {
std::cout << "Before coroutine\n";
asyncExample();
std::cout << "After coroutine (might print before coroutine completes)\n";
// Wait for detached threads to finish
std::this_thread::sleep_for(std::chrono::seconds(2));
}
Awaitable Traits
Types used with co_await must implement three methods or specialize std::await_ready, std::await_suspend, std::await_resume.
#include <iostream>
#include <coroutine>
// Manual awaitable with value return
struct IntProvider {
int value_;
bool await_ready() const noexcept { return false; }
// Return value controls suspension behavior:
// - void: unconditionally suspend
// - bool: true = suspend, false = don't suspend
// - coroutine_handle: resume a different coroutine
void await_suspend(std::coroutine_handle<> handle) {
std::cout << "Suspending coroutine\n";
// In real code: store handle, resume later
}
int await_resume() const noexcept {
return value_;
}
};
struct IntTask {
struct promise_type {
IntTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
IntTask testAwait() {
// co_await expression evaluates to await_resume() return value
int result = co_await IntProvider{42};
std::cout << "Got: " << result << "\n"; // 42
}
int main() {
testAwait();
}
Coroutine Frame and Lifetime
When a function body contains co_await, co_yield, or co_return, it becomes a coroutine. The compiler allocates a coroutine frame (heap-allocated by default) to hold the suspended state.
#include <iostream>
#include <coroutine>
// Demonstrate frame lifetime
struct LifecycleTask {
struct promise_type {
int id_;
promise_type() : id_(counter_++) {
std::cout << "Promise created (id=" << id_ << ")\n";
}
~promise_type() {
std::cout << "Promise destroyed (id=" << id_ << ")\n";
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
LifecycleTask get_return_object() {
return LifecycleTask{std::coroutine_handle<promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
static int counter_;
};
std::coroutine_handle<promise_type> handle_;
~LifecycleTask() {
if (handle_) {
std::cout << "Destroying task\n";
handle_.destroy();
}
}
};
int LifecycleTask::promise_type::counter_ = 0;
LifecycleTask lifecycleExample() {
std::cout << "Inside coroutine\n";
co_await std::suspend_always{};
std::cout << "After first suspend\n";
co_await std::suspend_always{};
std::cout << "After second suspend\n";
}
int main() {
auto task = lifecycleExample();
std::cout << "Back in main (coroutine suspended)\n";
task.handle_.resume(); // Resume first time
std::cout << "Back after first resume\n";
task.handle_.resume(); // Resume second time
std::cout << "Back after second resume\n";
}
Standard Awaitables: std::suspend_always and std::suspend_never
#include <iostream>
#include <coroutine>
// std::suspend_always: always suspends
// await_ready() → false
// await_suspend() → (suspends)
// await_resume() → void
// std::suspend_never: never suspends
// await_ready() → true
// await_suspend() → (never called)
// await_resume() → void
struct DemoTask {
struct promise_type {
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
DemoTask get_return_object() {
return {std::coroutine_handle<promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle_;
~DemoTask() { if (handle_) handle_.destroy(); }
};
DemoTask demo() {
std::cout << "Inside coroutine\n";
co_await std::suspend_never{}; // Does NOT suspend
std::cout << "Still inside (no suspend)\n";
co_await std::suspend_always{}; // Does suspend
std::cout << "After suspend\n";
}
int main() {
auto t = demo();
std::cout << "Main\n";
t.handle_.resume();
std::cout << "Main after resume\n";
}
Real-World: Generator with std::generator (C++23)
C++23 introduces std::generator<T> in <generator>.
#include <iostream>
#include <generator>
#include <ranges>
// C++23 std::generator (compile if available)
std::generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
int main() {
for (int x : range(0, 10) | std::views::filter([](int n) { return n % 2 == 0; })) {
std::cout << x << " ";
}
std::cout << "\n"; // 0 2 4 6 8
}
Common Mistakes
Mistake 1: Forgetting final_suspend
Without final_suspend, the coroutine frame may be destroyed before you can read the result.
Mistake 2: Not destroying the coroutine handle
auto h = coro(); // Gets a handle
// Forgets to call h.destroy() — memory leak!
Coroutine frames must be explicitly destroyed or stored in a RAII wrapper.
Mistake 3: Thinking coroutines are threads
Coroutines are not threads. They are suspendable functions that run on a single thread unless you explicitly schedule them on another.
Mistake 4: Using coroutines in hot loops with heap allocation
Allocating the coroutine frame on every iteration is expensive. Use std::generator or pool allocators for performance.
Mistake 5: Ignoring the promise_type contract
The promise type must implement initial_suspend, final_suspend, get_return_object, return_void or return_value, and unhandled_exception. Missing any causes compilation errors.
Practice Questions
What is a coroutine frame? Answer: A heap-allocated object that stores the suspended state of a coroutine, including local variables, the promise object, and resume point.
What does
co_yielddo? Answer: It suspends the coroutine and returns a value to the caller. The coroutine can be resumed later to produce the next value.What three methods must an awaitable implement? Answer:
await_ready,await_suspend, andawait_resume.What is the difference between
std::suspend_alwaysandstd::suspend_never? Answer:suspend_alwaysalways suspends.suspend_nevernever suspends. Usesuspend_neverfor initial suspend when you want the coroutine to start immediately.Can a coroutine have both
co_yieldandco_return? Answer: Yes.co_returnends the coroutine. Afterco_return,co_yieldis no longer valid.
FAQ
Mini Project
Build a coroutine-based Sequence generator that can produce arithmetic, geometric, and custom sequences:
#include <iostream>
#include <coroutine>
// Your Sequence<T> coroutine type
Sequence<int> arithmetic(int start, int step, int count) {
for (int i = 0; i < count; ++i) {
co_yield start + i * step;
}
}
Sequence<int> geometric(int start, int ratio, int count) {
int value = start;
for (int i = 0; i < count; ++i) {
co_yield value;
value *= ratio;
}
}
int main() {
// Arithmetic: 0, 5, 10, 15, 20
for (int val : arithmetic(0, 5, 5)) {
std::cout << val << " ";
}
std::cout << "\n";
// Geometric: 1, 2, 4, 8, 16
for (int val : geometric(1, 2, 5)) {
std::cout << val << " ";
}
std::cout << "\n";
}
This project demonstrates how C++ coroutines enable lazy, composable sequences — similar to generators in Python but with zero-allocation per iteration.
What's Next
You now understand coroutines — C++20's most powerful control flow feature. Next, you will learn modules (C++20), which replace header files with a modern, faster, and more hygienic compilation model.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro