C++20 Coroutines: A Beginner’s Guide

Coroutines have been a long‑awaited feature in the C++ standard library, providing a clean and efficient way to write asynchronous and lazy‑execution code without the overhead of traditional callbacks or thread management. With the release of C++20, coroutines have become officially part of the language, opening new possibilities for developers who want to write more expressive and maintainable code. In this article we will explore the basics of coroutines, how they are implemented in C++20, and a few practical examples that demonstrate their power.

What is a Coroutine?

A coroutine is a function that can suspend its execution at a specific point and resume later, potentially multiple times. Unlike regular functions that run to completion and return a value once, coroutines can pause and return control to the caller while keeping track of their state. When resumed, they continue from the point where they left off. This behavior is especially useful for:

  • Asynchronous programming – writing non‑blocking I/O code that looks like sequential code.
  • Lazy evaluation – generating values on demand, such as infinite streams.
  • Stateful iterators – simplifying complex iterator logic.

The C++20 Coroutine Syntax

In C++20, a coroutine is declared with the keyword co_await, co_yield, or co_return. These keywords are part of the coroutine specification:

  • co_await: suspends execution until the awaited awaitable completes.
  • co_yield: produces a value and suspends until the next call.
  • co_return: ends the coroutine, optionally returning a final value.

A coroutine function must return a coroutine type. Standard library types such as `std::generator

` or `std::future` can be used, but you can also define your own type. The compiler generates a state machine that handles the suspension and resumption logic. ### Basic Example: A Simple Generator “`cpp #include #include #include template struct generator { struct promise_type { T current_value; std::suspend_always yield_value(T value) { current_value = value; return {}; } std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } generator get_return_object() { return generator{ std::coroutine_handle ::from_promise(*this) }; } void return_void() {} void unhandled_exception() { std::exit(1); } }; std::coroutine_handle h; generator(std::coroutine_handle h) : h(h) {} ~generator() { if (h) h.destroy(); } struct iterator { std::coroutine_handle h; bool operator!=(std::default_sentinel_t) { return h.done() == false; } void operator++() { h.resume(); } T operator*() { return h.promise().current_value; } }; iterator begin() { h.resume(); return {h}; } std::default_sentinel_t end() { return {}; } }; generator numbers() { for (int i = 0; i < 5; ++i) co_yield i; } “` This generator produces the numbers 0 through 4. Each call to `co_yield` suspends the coroutine, returning a value to the caller. When the caller advances the iterator, the coroutine resumes from where it left off. ## Asynchronous File I/O with Coroutines A more practical use case for coroutines is asynchronous I/O. Using the ` ` library or any async I/O library that provides awaitable objects, you can write code that feels synchronous but is actually non‑blocking. “`cpp #include #include asio::awaitable async_read_file(asio::io_context& ctx, const std::string& path) { asio::async_file file(path, asio::file_base::read, asio::use_awaitable); std::vector buffer(1024); std::size_t bytes_read = co_await file.async_read_some(asio::buffer(buffer), asio::use_awaitable); std::cout << "Read " << bytes_read << " bytes\n"; } “` The `async_read_file` function suspends when awaiting the file read operation. When the I/O completes, the coroutine automatically resumes. No threads are blocked during the wait, and the code remains readable. ## Error Handling in Coroutines Exceptions propagate normally across `co_await` points. However, you can also design awaitable objects that return error codes instead of throwing. Coroutines can also catch exceptions: “`cpp generator safe_numbers() { try { for (int i = 0; i < 5; ++i) co_yield i; } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << '\n'; co_return; } } “` ## Performance Considerations While coroutines add a small overhead in terms of the generated state machine, they can actually reduce runtime cost by eliminating callbacks and thread switches. The compiler-generated code is highly optimized, and when combined with in‑place allocation strategies (e.g., `std::promise_type` using a preallocated buffer), you can achieve performance on par with hand‑written state machines. ## Conclusion C++20 coroutines provide a powerful abstraction for asynchronous and lazy execution, enabling developers to write cleaner, more maintainable code. By understanding the coroutine syntax, promise types, and awaitable objects, you can integrate this feature into your projects for tasks ranging from simple generators to complex asynchronous I/O pipelines. As the ecosystem matures, expect to see even more standard awaitable types and libraries that make coroutines accessible to a broader range of applications. Happy coding!

发表评论