Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions doc/unlisted/library-io-result.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,57 @@ io_task<std::size_t> read_all(stream& s, buffer& buf)
}
----

== Short-Circuiting with co_yield

Inside an `io_task<Ts...>`, `co_yield` accepts an `io_result<Us...>` 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<Us...>` of the payload values and execution continues.
* On error: the task completes immediately, returning
`io_result<Ts...>{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<std::size_t> echo(stream& s, mutable_buffer buf)
{
// Without co_yield:
// auto [ec, n] = co_await s.read_some(buf);
// if (ec) co_return io_result<std::size_t>{ec, 0};
// auto [ec2, w] = co_await s.write_some(buf.first(n));
// if (ec2) co_return io_result<std::size_t>{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<std::size_t>{{}, 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<std::size_t> 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:**
Expand Down
142 changes: 141 additions & 1 deletion include/boost/capy/io_task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include <boost/capy/io_result.hpp>
#include <boost/capy/task.hpp>
#include <coroutine>

namespace boost {
namespace capy {
Expand All @@ -37,7 +38,146 @@ namespace capy {
@tparam Ts Additional value types beyond error_code.
*/
template<class... Ts>
using io_task = task<io_result<Ts...>>;
struct io_task
{
struct promise_type : task<io_result<Ts...>>::promise_type
{
io_task get_return_object()
{
return io_task{std::coroutine_handle<promise_type>::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<typename ... Us>
requires (std::constructible_from<Ts> && ...)
auto yield_value(io_result<Us...> res)
{
struct awaiter
{
io_result<Us...> res;
bool await_ready() {return !res.ec;}

std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h)
{
auto &p = h.promise();
p.return_value({res.ec, Ts()...});

return p.continuation();
}

std::tuple<Us...> 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<promise_type> 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<promise_type> h)
: h_(h)
{
}

std::coroutine_handle<promise_type> h_;


};

} // namespace capy
} // namespace boost
Expand Down
42 changes: 41 additions & 1 deletion include/boost/capy/task.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <boost/capy/ex/frame_allocator.hpp>
#include <boost/capy/detail/await_suspend_helper.hpp>

#include <concepts>
#include <exception>
#include <optional>
#include <type_traits>
Expand All @@ -27,6 +28,9 @@
namespace boost {
namespace capy {

template<typename ... Ts>
struct io_result;

namespace detail {

// Helper base for result storage and return_void/return_value
Expand Down Expand Up @@ -54,6 +58,12 @@ struct task_return_base<void>
}
};

template<typename ... Us>
void handle_yield_result(io_result<Us...> & res, std::error_code ec)
{
static_assert((std::constructible_from<Us> && ...), "co_yield requires all result value to be default constructible");
}

} // namespace detail

/** Lazy coroutine task satisfying @ref IoRunnable.
Expand Down Expand Up @@ -102,7 +112,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
: io_awaitable_promise_base<promise_type>
, detail::task_return_base<T>
{
private:
protected:
friend task;
union { std::exception_ptr ep_; };
bool has_ep_;
Expand Down Expand Up @@ -227,6 +237,36 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
static_assert(sizeof(A) == 0, "requires IoAwaitable");
}
}



template<class ... Ts>
auto yield_value(io_result<Ts...> res)
{
struct awaitable
{
io_result<Ts...> res;

bool await_ready() const {return !res.ec;}
void await_suspend(std::coroutine_handle<promise_type> h)
{
auto & p = h.promise();
try
{
detail::handle_yield_result(h.promise(), res.ec);
}
catch (...)
{
p.uncaught_exception();
}

}
std::tuple<Ts...> await_resume()
{
return std::move(res.values);
}
};
}
};

std::coroutine_handle<promise_type> h_;
Expand Down
Loading
Loading