From 51c61389878d6f37a997f3d14313da2baab25c27 Mon Sep 17 00:00:00 2001 From: Klemens Morgenstern Date: Thu, 4 Jun 2026 22:59:48 +0800 Subject: [PATCH] io_result can be co_yielded in io_task --- doc/unlisted/library-io-result.adoc | 51 ++++++++ include/boost/capy/io_task.hpp | 142 +++++++++++++++++++++- include/boost/capy/task.hpp | 42 ++++++- test/unit/io_task.cpp | 181 ++++++++++++++++++++++++++++ 4 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 test/unit/io_task.cpp diff --git a/doc/unlisted/library-io-result.adoc b/doc/unlisted/library-io-result.adoc index 5503c1265..af5a71dc3 100644 --- a/doc/unlisted/library-io-result.adoc +++ b/doc/unlisted/library-io-result.adoc @@ -189,6 +189,57 @@ io_task read_all(stream& s, buffer& buf) } ---- +== Short-Circuiting with co_yield + +Inside an `io_task`, `co_yield` accepts an `io_result` and +collapses the "check `ec`, propagate on error, otherwise use the values" +pattern into a single expression: + +* On success (`!ec`): the `co_yield` expression evaluates to + `std::tuple` of the payload values and execution continues. +* On error: the task completes immediately, returning + `io_result{ec, Ts{}...}` to its caller. Each `Ts` must be + default-constructible. + +The pattern is most useful when chaining many fallible operations: + +[source,cpp] +---- +io_task echo(stream& s, mutable_buffer buf) +{ + // Without co_yield: + // auto [ec, n] = co_await s.read_some(buf); + // if (ec) co_return io_result{ec, 0}; + // auto [ec2, w] = co_await s.write_some(buf.first(n)); + // if (ec2) co_return io_result{ec2, 0}; + + auto [n] = co_yield co_await s.read_some(buf); + auto [w] = co_yield co_await s.write_some(buf.first(n)); + co_return io_result{{}, w}; +} +---- + +When the task returns its own payload values (`Ts...` non-empty), those +values are default-constructed on the error path. Use explicit +`co_return` when you need to surface a partially-computed value instead: + +[source,cpp] +---- +io_task read_all(stream& s, buffer& buf) +{ + std::size_t total = 0; + while (buf.size() < buf.max_size()) + { + auto [ec, n] = co_await s.read_some(buf.prepare(1024)); + if (ec) + co_return {ec, total}; // co_yield would lose `total` + buf.commit(n); + total += n; + } + co_return {{}, total}; +} +---- + == Error Code vs Exception Trade-offs **Prefer error codes when:** diff --git a/include/boost/capy/io_task.hpp b/include/boost/capy/io_task.hpp index b3067c2c2..2b366513a 100644 --- a/include/boost/capy/io_task.hpp +++ b/include/boost/capy/io_task.hpp @@ -12,6 +12,7 @@ #include #include +#include namespace boost { namespace capy { @@ -37,7 +38,146 @@ namespace capy { @tparam Ts Additional value types beyond error_code. */ template -using io_task = task>; +struct io_task +{ + struct promise_type : task>::promise_type + { + io_task get_return_object() + { + return io_task{std::coroutine_handle::from_promise(*this)}; + } + + friend io_task; + + // An io_task can yield an io_result value. That means the coroutie suspend if the error is set. + template + requires (std::constructible_from && ...) + auto yield_value(io_result res) + { + struct awaiter + { + io_result res; + bool await_ready() {return !res.ec;} + + std::coroutine_handle<> await_suspend(std::coroutine_handle h) + { + auto &p = h.promise(); + p.return_value({res.ec, Ts()...}); + + return p.continuation(); + } + + std::tuple await_resume() + { + return std::move(res.values); + } + }; + + return awaiter{std::move(res)}; + } + + + }; + + /// Destroy the task and its coroutine frame if owned. + + ~io_task() + { + if (h_) + h_.destroy(); + } + /// Return false; tasks are never immediately ready. + bool await_ready() const noexcept + { + return false; + } + + /// Return the result or rethrow any stored exception. + auto await_resume() + { + if(h_.promise().has_ep_) + std::rethrow_exception(h_.promise().ep_); + return std::move(*h_.promise().result_); + } + + + + /// Start execution with the caller's context. + std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, io_env const* env) + { + h_.promise().set_continuation(cont); + h_.promise().set_environment(env); + return h_; + } + + /** Return the coroutine handle. + + @note Do not call `destroy()` on the returned handle while the + task is being awaited. The task's lifetime is normally managed + by `run_async`, `run`, or the awaiting parent; manually + destroying a suspended task that another coroutine is awaiting + produces undefined behavior. For cooperative cancellation, use + `std::stop_token`. + + @return The coroutine handle. + */ + std::coroutine_handle handle() const noexcept + { + return h_; + } + + /** Release ownership of the coroutine frame. + + After calling this, destroying the task does not destroy the + coroutine frame. The caller becomes responsible for the frame's + lifetime. + + @note If the caller intends to call `destroy()` on the + released handle, it must do so only when the task has not + started or has fully completed. Destroying a suspended task + that is being awaited produces undefined behavior. + + @par Postconditions + `handle()` returns the original handle, but the task no longer + owns it. + */ + void release() noexcept + { + h_ = nullptr; + } + + io_task(io_task const&) = delete; + io_task& operator=(io_task const&) = delete; + + /// Construct by moving, transferring ownership. + io_task(io_task&& other) noexcept + : h_(std::exchange(other.h_, nullptr)) + { + } + + /// Assign by moving, transferring ownership. + io_task& operator=(io_task&& other) noexcept + { + if(this != &other) + { + if(h_) + h_.destroy(); + h_ = std::exchange(other.h_, nullptr); + } + return *this; + } + + + private: + explicit io_task(std::coroutine_handle h) + : h_(h) + { + } + + std::coroutine_handle h_; + + +}; } // namespace capy } // namespace boost diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index ce2a5b556..f9ebc7b05 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,9 @@ namespace boost { namespace capy { +template +struct io_result; + namespace detail { // Helper base for result storage and return_void/return_value @@ -54,6 +58,12 @@ struct task_return_base } }; +template +void handle_yield_result(io_result & res, std::error_code ec) +{ + static_assert((std::constructible_from && ...), "co_yield requires all result value to be default constructible"); +} + } // namespace detail /** Lazy coroutine task satisfying @ref IoRunnable. @@ -102,7 +112,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE : io_awaitable_promise_base , detail::task_return_base { - private: + protected: friend task; union { std::exception_ptr ep_; }; bool has_ep_; @@ -227,6 +237,36 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE static_assert(sizeof(A) == 0, "requires IoAwaitable"); } } + + + + template + auto yield_value(io_result res) + { + struct awaitable + { + io_result res; + + bool await_ready() const {return !res.ec;} + void await_suspend(std::coroutine_handle h) + { + auto & p = h.promise(); + try + { + detail::handle_yield_result(h.promise(), res.ec); + } + catch (...) + { + p.uncaught_exception(); + } + + } + std::tuple await_resume() + { + return std::move(res.values); + } + }; + } }; std::coroutine_handle h_; diff --git a/test/unit/io_task.cpp b/test/unit/io_task.cpp new file mode 100644 index 000000000..0b7bbe42a --- /dev/null +++ b/test/unit/io_task.cpp @@ -0,0 +1,181 @@ +// +// Copyright (c) 2026 Klemens Morgenstern +// +// 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) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include + +#include "test_suite.hpp" + +#include +#include + +namespace boost { +namespace capy { + +struct io_task_yield_test +{ + //---------------------------------------------------------- + // co_yield io_result<> with no error: continues, returns + // the payload tuple from the yield expression. + //---------------------------------------------------------- + + static io_task + yields_success_single() + { + auto [v] = co_yield io_result{{}, 7}; + co_return io_result{{}, v + 1}; + } + + void + testYieldSuccessSingleValue() + { + io_result result; + test::run_blocking([&](io_result r) { + result = std::move(r); + })(yields_success_single()); + + BOOST_TEST(!result.ec); + BOOST_TEST_EQ(std::get<0>(result.values), 8); + } + + static io_task + yields_success_multi() + { + auto [a, b, c] = co_yield io_result{{}, 1, 2, 3}; + co_return io_result{{}, a + b + c}; + } + + void + testYieldSuccessMultipleValues() + { + io_result result; + test::run_blocking([&](io_result r) { + result = std::move(r); + })(yields_success_multi()); + + BOOST_TEST(!result.ec); + BOOST_TEST_EQ(std::get<0>(result.values), 6); + } + + static io_task + yields_success_string() + { + auto [s] = co_yield io_result{{}, std::string("hello")}; + co_return io_result{{}, s + " world"}; + } + + void + testYieldSuccessStringPayload() + { + io_result result; + test::run_blocking([&](io_result r) { + result = std::move(r); + })(yields_success_string()); + + BOOST_TEST(!result.ec); + BOOST_TEST_EQ(std::get<0>(result.values), "hello world"); + } + + //---------------------------------------------------------- + // co_yield io_result<> with an error short-circuits: the + // remainder of the coroutine after the yield must not run. + //---------------------------------------------------------- + + static io_task + yields_error(bool* after_yield_ran) + { + auto ec = make_error_code(std::errc::invalid_argument); + auto [v] = co_yield io_result{ec, 99}; + *after_yield_ran = true; + co_return io_result{{}, v}; + } + + void + testYieldErrorShortCircuits() + { + bool after_yield_ran = false; + io_result result; + + test::run_blocking([&](io_result r) { + result = std::move(r); + })(yields_error(&after_yield_ran)); + + BOOST_TEST(!after_yield_ran); + BOOST_TEST(result.ec == make_error_code(std::errc::invalid_argument)); + } + + //---------------------------------------------------------- + // Yielding void io_result (no payload) also short-circuits + // on error and continues otherwise. + //---------------------------------------------------------- + + static io_task<> + yields_void_success(bool* after_yield_ran) + { + co_yield io_result<>{}; + *after_yield_ran = true; + co_return io_result<>{}; + } + + void + testYieldVoidSuccessContinues() + { + bool after_yield_ran = false; + io_result<> result{make_error_code(std::errc::invalid_argument)}; + + test::run_blocking([&](io_result<> r) { + result = std::move(r); + })(yields_void_success(&after_yield_ran)); + + BOOST_TEST(after_yield_ran); + BOOST_TEST(!result.ec); + } + + static io_task<> + yields_void_error(bool* after_yield_ran) + { + auto ec = make_error_code(std::errc::operation_canceled); + co_yield io_result<>{ec}; + *after_yield_ran = true; + co_return io_result<>{}; + } + + void + testYieldVoidErrorShortCircuits() + { + bool after_yield_ran = false; + io_result<> result; + + test::run_blocking([&](io_result<> r) { + result = std::move(r); + })(yields_void_error(&after_yield_ran)); + + BOOST_TEST(!after_yield_ran); + BOOST_TEST(result.ec == make_error_code(std::errc::operation_canceled)); + } + + void + run() + { + testYieldSuccessSingleValue(); + testYieldSuccessMultipleValues(); + testYieldSuccessStringPayload(); + testYieldErrorShortCircuits(); + testYieldVoidSuccessContinues(); + testYieldVoidErrorShortCircuits(); + } +}; + +TEST_SUITE(io_task_yield_test, "boost.capy.io_task"); + +} // namespace capy +} // namespace boost