C++20 introduced coroutines, a language feature that lets you write asynchronous, lazy, and streaming code in a natural, synchronous-looking style. Coroutines enable the definition of functions that can suspend execution and resume later, making it easier to build pipelines, generators, and asynchronous APIs without the boilerplate of callbacks or state machines.
1. What Is a Coroutine?
A coroutine is a function that can pause (co_await, co_yield, co_return) and later resume from the same point. Under the hood, the compiler transforms the coroutine into a state machine that preserves local variables across suspensions.
co_await– Suspends until an awaited awaitable completes.co_yield– Produces a value and suspends, similar toyieldin Python.co_return– Returns a value and finishes the coroutine.
Coroutines are typically used in three patterns:
- Generator – Produces a sequence of values lazily.
- Task/Async – Represents an asynchronous operation.
- Pipeline – Chains multiple suspensions for data processing.
2. Generators: Lazy Sequences
A generator returns a value each time it is resumed. The result type must be a custom awaitable or generator type. The standard library does not yet provide a full generator type, but many libraries (e.g., cppcoro, Boost.Coroutine2) offer implementations. Below is a minimal custom generator:
#include <iostream>
#include <coroutine>
#include <exception>
template<typename T>
struct Generator {
struct promise_type {
T current_value;
std::exception_ptr exception;
Generator get_return_object() {
return Generator{
.handle = std::coroutine_handle <promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { exception = std::current_exception(); }
};
std::coroutine_handle <promise_type> handle;
explicit Generator(std::coroutine_handle <promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
struct Iterator {
std::coroutine_handle <promise_type> coro;
bool done = false;
Iterator(std::coroutine_handle <promise_type> c, bool d) : coro(c), done(d) {}
Iterator& operator++() {
coro.resume();
if (coro.done() || coro.promise().exception)
done = true;
return *this;
}
T operator*() const { return coro.promise().current_value; }
bool operator!=(const Iterator& other) const { return done != other.done; }
};
Iterator begin() { return Iterator{handle, !handle || handle.done()}; }
Iterator end() { return Iterator{handle, true}; }
};
Generator <int> natural_numbers(int start = 0) {
int n = start;
while (true) {
co_yield n++;
}
}
int main() {
auto gen = natural_numbers(5);
int count = 0;
for (auto n : gen) {
std::cout << n << ' ';
if (++count == 10) break; // manual stop
}
}
This generator lazily produces natural numbers starting from 5. The loop stops after ten numbers to avoid an infinite sequence.
3. Async Tasks: Non-Blocking Operations
C++20’s std::future and std::async provide basic asynchronous facilities, but coroutines allow a cleaner integration with event loops and I/O libraries.
A simple Task type that returns a value asynchronously:
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
template<typename T>
struct Task {
struct promise_type {
T value;
std::exception_ptr exception;
Task get_return_object() {
return Task{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exception = std::current_exception(); }
};
std::coroutine_handle <promise_type> handle;
Task(std::coroutine_handle <promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
T get() {
handle.resume();
if (handle.promise().exception) std::rethrow_exception(handle.promise().exception);
return handle.promise().value;
}
};
Task <int> async_add(int a, int b) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
co_return a + b;
}
int main() {
auto result = async_add(3, 4).get();
std::cout << "Result: " << result << '\n';
}
Here async_add simulates a long-running operation using sleep_for. In real-world code, you would replace this with non-blocking I/O.
4. Pipelines: Composing Coroutines
Coroutines can be chained to build data pipelines. For example, read lines from a file, transform them, and write them elsewhere.
#include <fstream>
#include <string>
#include <iostream>
#include <coroutine>
template<typename T>
struct Stream {
struct promise_type { ... }; // similar to Generator
};
Stream<std::string> read_lines(std::istream& in) {
std::string line;
while (std::getline(in, line)) {
co_yield line;
}
}
Stream<std::string> to_upper(std::string input) {
for (auto& c : input) c = std::toupper(c);
co_return std::move(input);
}
int main() {
std::ifstream file("input.txt");
for (auto line : read_lines(file)) {
std::string upper = to_upper(line).get(); // convert to upper case
std::cout << upper << '\n';
}
}
By separating concerns, each coroutine handles a specific transformation, and the main loop stays clean.
5. Integrating with Libraries
Many modern libraries now support coroutines:
- Boost.Coroutine2 – Provides a robust coroutine infrastructure.
- cppcoro – Offers
generator,task,sync_wait. - Asio – Asynchronous I/O with
co_awaitin C++20.
Using these, you can avoid writing boilerplate state machines and handle errors elegantly with exceptions.
6. Tips and Best Practices
- Return Types – Prefer `generator ` for streams and `task` for async results.
- Error Handling – Store exceptions in the promise and rethrow on
get(). - Resource Management – Ensure handles are destroyed; the coroutine handles its own stack.
- Performance – Avoid excessive
co_yieldin tight loops; the state machine overhead is minimal but non-zero. - Testing – Treat coroutines as regular functions; you can step through with debuggers that support coroutines.
7. Conclusion
C++20 coroutines elevate asynchronous and lazy programming to a new level of expressiveness and simplicity. By leveraging generators, async tasks, and pipelines, developers can write clearer code that mimics synchronous styles while still benefiting from non-blocking execution. As the ecosystem matures, expect more libraries to embrace coroutines, making this feature a staple for modern C++ development.