[/ / Copyright (c) 2021-2023 Klemens D. Morgenstern / (klemens dot morgenstern at gmx dot net) / / Distributed under the Boost Software License, Version 1.0. (See accompanying / file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) /] [section:coro Resumable C++ 20 Coroutines] [note This is an experimental feature.] The [link boost_asio.reference.experimental__coro `experimental::coro`] class provides support for a universal C++20 coroutine. These coroutines can be used as tasks, generators and transfomers, depending on their signature. coro<std::string_view> line_reader(tcp::socket stream) { while (stream.is_open()) { std::array<char, 4096> buf; auto read = co_await stream.async_read_some( boost::asio::buffer(buf), deferred); if (read == 0u) continue; co_yield std::string_view { buf.data(), read }; } } coro<void, std::size_t> line_logger(tcp::socket stream) { std::size_t lines_read = 0u; auto reader = line_reader(std::move(stream)); while (auto l = co_await reader) { std::cout << "Read: '" << *l << "'" << std::endl; lines_read++; } co_return lines_read; } void read_lines(tcp::socket sock) { co_spawn(line_logger(std::move(sock), [](std::exception_ptr, std::size_t lines) { std::clog << "Read " << lines << " lines" << std::endl; })); } A [link boost_asio.reference.experimental__coro `coro`] is highly configurable, so that it can cover a set of different use cases. template< typename Yield, typename Return = void, typename Executor = any_io_executor> struct coro; [heading Yield] The `Yield` parameter designates how a `co_yield` statement behaves. It can either be a type, like `int` or a signature with zero or one types: coro<void> // A coroutine with no yield coro<int> // A coroutine that can yield int coro<void()> // A coroutine with no yield coro<int()> // A coroutine that can yield int coro<int(double)> // A coroutine that can yield int and receive double Receiving a value means that the `co_yield` statement returns a value. coro<int(int)> my_sum(any_io_executor) { int value = 0; while (true) value += co_yield value; //sum up all values } Putting values into a coroutine can be done it two ways: either by direct resumption (from another coro) or through async_resume. The first value gets ignored because the coroutines are lazy. coro<void> c(any_io_executor exec) { auto sum = my_sum(exec); assert(0 == co_await sum(-1)); assert(0 == co_await sum(10)); assert(10 == co_await sum(15)); assert(25 == co_await sum(0)); } awaitable<void> a() { auto sum = my_sum(co_await this_coro::executor); assert(0 == co_await sum.async_resume(-1, use_awaitable)); assert(0 == co_await sum.async_resume(10, use_awaitable)); assert(10 == co_await sum.async_resume(15, use_awaitable)); assert(25 == co_await sum.async_resume(0, use_awaitable)); } [heading `noexcept`] A coro may be noexcept: coro<void() noexcept> c; coro<int() noexcept> c; coro<int(double) noexcept> c; This will change its @c async_resume signature, from `void(std::exception_ptr)` to `void()` or `void(std::exception_ptr, T)` to `void(T)`. A noexcept coro that ends with an exception will cause `std::terminate` to be called. Furthermore, calls of `async_resume` and `co_await` of an expired noexcept coro will cause undefined behaviour. [heading Return] A coro can also define a type that can be used with `co_return`: coro<void() noexcept, int> c(any_io_executor) { co_return 42; } A coro can have both a `Yield` and `Return` that are non void at the same time. [heading Result] The result type of a coroutine is dermined by both `Yield` and `Return`. Note that in the follwing table only the yield output value is considered, i.e. `T(U)` means `T`. [table:result_type Result type deduction [[Yield] [Return] [`noexcept`] [`result_type`] [`completion_signature`]] [[`T`] [`U`] [`false`] [`variant<T, U>`] [`void(std::exception_ptr, variant<T, U>)`]] [[`T`] [`U`] [`true`] [`variant<T, U>`] [`void(variant<T, U>)`]] [[`T`] [`void`] [`false`] [`optional<T>`] [`void(std::exception_ptr, optional<T>)`]] [[`T`] [`void`] [`true`] [`optional<T>`] [`void(optional<T>)`]] [[`void`] [`void`] [`false`] [`optional<T>`] [`void(std::exception_ptr)`]] [[`void`] [`void`] [`true`] [`optional<T>`] [`void()`]] [[`void`] [`T`] [`false`] [`optional<T>`] [`void(std::exception_ptr, T)`]] [[`void`] [`T`] [`true`] [`optional<T>`] [`void(T)`]] ] [heading Executor] Every coroutine needs to have its own executor. Since the coroutine gets called multiple times, it cannot take the executor from the caller like an `awaitable`. Therefore a `coro` requires to get an executor or an execution_context passed in as the first parameter. coro<int> with_executor(any_io_executor); coro<int> with_context(io_context &); It is to note, that an execution_context is defined as loosely as possible. An execution context is any object that has a `get_executor()` function, which returns an executor that can be transformed into the executor_type of the coroutine. This allows most io_objects to be used as the source of the executor: coro<int> with_socket(tcp::socket); Additionally, a `coro` that is a member function will check the `this` pointer as well, either if it's an executor or an execution context: struct my_io_object { any_io_executor get_executor(); coro<int> my_coro(); }; Finally, a member coro can be given an explicit executor or execution context, to override the one of the object: struct my_io_object { any_io_executor get_executor(); coro<int> my_coro(any_io_executor exec); // it will use exec }; [heading `co_await`] The @c co_await within a `coro` is not the same as `async_resume(deferred)`, unless both coros use different executors. If they use the same, the `coro` will direclty suspend and resume the executor, without any usage of the executor. `co_await this_coro::` behaves the same as coroutines that use @c boost::asio::awaitable. [heading Integrating with awaitable] As the `coro` member function `async_resume` is an asynchronous operation, it may also be used in conjunction with `awaitable` coroutines in a single control flow. For example: #include <asio.hpp> #include <boost/asio/experimental/coro.hpp> using boost::asio::ip::tcp; boost::asio::experimental::coro<std::string> reader(tcp::socket& sock) { std::string buf; while (sock.is_open()) { std::size_t n = co_await boost::asio::async_read_until( sock, boost::asio::dynamic_buffer(buf), '\n', boost::asio::deferred); co_yield buf.substr(0, n); buf.erase(0, n); } } boost::asio::awaitable<void> consumer(tcp::socket sock) { auto r = reader(sock); auto msg1 = co_await r.async_resume(boost::asio::use_awaitable); std::cout << "Message 1: " << msg1.value_or("\n"); auto msg2 = co_await r.async_resume(boost::asio::use_awaitable); std::cout << "Message 2: " << msg2.value_or("\n"); } boost::asio::awaitable<void> listen(tcp::acceptor& acceptor) { for (;;) { co_spawn( acceptor.get_executor(), consumer(co_await acceptor.async_accept(boost::asio::use_awaitable)), boost::asio::detached); } } int main() { boost::asio::io_context ctx; tcp::acceptor acceptor(ctx, {tcp::v4(), 54321}); co_spawn(ctx, listen(acceptor), boost::asio::detached); ctx.run(); } [heading See Also] [link boost_asio.reference.co_spawn co_spawn], [link boost_asio.reference.experimental__coro experimental::coro], [link boost_asio.overview.composition.cpp20_coroutines C++20 Coroutines], [link boost_asio.overview.composition.spawn Stackful Coroutines], [link boost_asio.overview.composition.coroutine Stackless Coroutines]. [endsect]