From 7a926d2502192c2578156f9b5385718d60931f95 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 4 Aug 2026 15:37:12 +0200 Subject: [PATCH] refactor(example): make channel splitting structural rather than nominal Key the awaitable-sender's error_code splitting on shape instead of the io_result name, per P4093: any tuple-like whose element 0 is error_code is an outcome. Single-element outcomes split across the value/error channels; compound ones (error_code plus payload) are rejected at compile time regardless of type name, since exclusive completion channels cannot carry a partial success without dropping data. Replace the io_result arity trait with tuple-protocol probes and update the shape coverage in the static assertions. --- example/awaitable-sender/awaitable_sender.hpp | 40 +++++------ .../awaitable_sender_detail.hpp | 67 ++++++++++++------- example/awaitable-sender/tests.cpp | 38 +++++------ 3 files changed, 83 insertions(+), 62 deletions(-) diff --git a/example/awaitable-sender/awaitable_sender.hpp b/example/awaitable-sender/awaitable_sender.hpp index 1fc9cadbb..29b0a49c5 100644 --- a/example/awaitable-sender/awaitable_sender.hpp +++ b/example/awaitable-sender/awaitable_sender.hpp @@ -76,19 +76,21 @@ struct awaitable_sender channels based on its type: - `void` - calls `set_value()`. - - `error_code` or an empty `io_result` - calls - `set_value()` when the code is zero, `set_error(ec)` - otherwise. - - Any other single value `T` - calls `set_value(T)`, - including generic tuple-likes that happen to lead with - an `error_code`: only `io_result` declares the - element-0-is-outcome intent, so only it is split. - - An `io_result` with payload elements is rejected at compile - time. Completion channels are exclusive, so a partial - success (an `error_code` arriving alongside bytes already - transferred) cannot be delivered on any single channel - without dropping data. Wrap such an operation in a + - `error_code`, or any single-element tuple-like whose + sole element is `error_code` (such as an empty + `io_result`) - calls `set_value()` when the code is + zero, `set_error(ec)` otherwise. + - Any other single value `T` - calls `set_value(T)`. + + Compound results are rejected at compile time, and the + constraint is structural, not nominal: any tuple-like whose + element 0 is `error_code` with additional elements is + refused, whether it is `io_result`, + `std::tuple`, or a user-defined type of + the same shape. Completion channels are exclusive, so a + partial success (an `error_code` arriving alongside bytes + already transferred) cannot be delivered on any single + channel without dropping data. Wrap such an operation in a `task` that inspects the full result, moves the payload out through a side channel, and returns the code. @@ -116,12 +118,12 @@ auto as_sender(IoAw&& aw) using R = awaitable_result_t>; static_assert( !detail::is_compound_ec_result_v, - "as_sender does not accept awaitables whose result is an " - "io_result with payload elements: completion channels " - "are exclusive, so a partial success (error_code plus " - "payload) would be silently dropped. Wrap the operation " - "in a task that inspects the full result " - "and returns the error code."); + "as_sender does not accept awaitables whose result " + "destructures into (error_code, ...): completion " + "channels are exclusive, so a partial success " + "(error_code plus payload) would be silently dropped. " + "Wrap the operation in a task that inspects " + "the full result and returns the error code."); return awaitable_sender>{ std::forward(aw)}; } diff --git a/example/awaitable-sender/awaitable_sender_detail.hpp b/example/awaitable-sender/awaitable_sender_detail.hpp index 48405ccf5..2f6c21a39 100644 --- a/example/awaitable-sender/awaitable_sender_detail.hpp +++ b/example/awaitable-sender/awaitable_sender_detail.hpp @@ -81,32 +81,53 @@ struct io_sender_env namespace detail { -// Channel splitting keys on capy's io_result, the type that -// declares "element 0 is an error_code" as intent. A generic -// tuple-like that merely happens to lead with an error_code is a -// value, not an outcome to split — shape alone cannot tell a -// result protocol from a payload. An io_result with payload -// elements is rejected in make_sigs: exclusive completion -// channels cannot carry a partial success without dropping data. -// The arity trait avoids naming std::tuple_size on foreign +// Channel splitting is structural, not nominal (P4093): any +// tuple-like whose element 0 is error_code is an outcome, no +// matter its name. A bare error_code or a single-element +// tuple-like is a binary outcome and splits across the +// channels; one with payload elements is rejected in +// make_sigs, because exclusive completion channels cannot +// carry a partial success without dropping data. The SFINAE +// probe avoids naming std::tuple_size members on foreign // types, which would hard-error for non-tuples (&& does not // short-circuit instantiation). +template +struct has_tuple_protocol : std::false_type {}; + +template +struct has_tuple_protocol::type>> + : std::bool_constant<(std::tuple_size::value > 0)> {}; + +template::value> +struct is_ec_outcome + : std::is_same {}; + +template +struct is_ec_outcome + : std::bool_constant< + std::tuple_size_v == 1 && + std::is_same_v< + std::tuple_element_t<0, T>, + std::error_code>> {}; + template -struct io_result_arity - : std::integral_constant {}; +constexpr bool is_ec_outcome_v = is_ec_outcome::value; -template -struct io_result_arity> - : std::integral_constant {}; +template::value> +struct is_compound_ec_result : std::false_type {}; template -constexpr bool is_ec_outcome_v = - std::is_same_v || - io_result_arity::value == 1; +struct is_compound_ec_result + : std::bool_constant< + std::tuple_size_v >= 2 && + std::is_same_v< + std::tuple_element_t<0, T>, + std::error_code>> {}; template constexpr bool is_compound_ec_result_v = - io_result_arity::value >= 2; + is_compound_ec_result::value; // ------------------------------------------------------- // frame_cb: synthetic coroutine frame for callback handles @@ -140,12 +161,12 @@ auto make_sigs() static_assert( !is_compound_ec_result_v, - "IoAwaitables whose result is an io_result with payload " - "elements cannot be senders: completion channels are " - "exclusive, so a partial success (error_code plus " - "payload) would be silently dropped. Wrap the operation " - "in a task that inspects the full result " - "and returns the error code."); + "IoAwaitables whose result destructures into " + "(error_code, ...) cannot be senders: completion " + "channels are exclusive, so a partial success " + "(error_code plus payload) would be silently dropped. " + "Wrap the operation in a task that inspects " + "the full result and returns the error code."); constexpr bool nothrow_resume = noexcept(std::declval().await_resume()); diff --git a/example/awaitable-sender/tests.cpp b/example/awaitable-sender/tests.cpp index b1403b3ee..a5182f4e4 100644 --- a/example/awaitable-sender/tests.cpp +++ b/example/awaitable-sender/tests.cpp @@ -796,26 +796,24 @@ void test_coawait_stopped_midflight() CHECK(coawait_out.ch == channel::stopped); } -// Channel splitting is keyed on io_result, not tuple shape: a -// std::tuple that happens to lead with error_code is a value. -struct tuple_result_op -{ - bool await_ready() const noexcept { return false; } - auto await_suspend( - std::coroutine_handle<>, capy::io_env const*) - { - return std::noop_coroutine(); - } - std::tuple await_resume() noexcept - { - return {}; - } -}; -static_assert(std::is_same_v< - decltype(capy::detail::make_sigs()), - ex::completion_signatures< - ex::set_value_t(std::tuple), - ex::set_stopped_t()>>); +// Channel splitting is structural, not nominal (P4093): any +// tuple-like leading with error_code splits when it carries no +// payload and is rejected when it does, io_result or not. +static_assert(capy::detail::is_ec_outcome_v); +static_assert(capy::detail::is_ec_outcome_v>); +static_assert(capy::detail::is_ec_outcome_v< + std::tuple>); +static_assert(capy::detail::is_compound_ec_result_v< + capy::io_result>); +static_assert(capy::detail::is_compound_ec_result_v< + std::tuple>); +static_assert(capy::detail::is_compound_ec_result_v< + std::pair>); +// no tuple protocol, no error_code lead: plain values +static_assert(!capy::detail::is_ec_outcome_v && + !capy::detail::is_compound_ec_result_v); +static_assert(!capy::detail::is_compound_ec_result_v< + std::pair>); // AwaitableSender partitions the world correctly: read_op models // both protocols; compound_op is awaitable-only; the as_sender