Skip to content
Merged
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
12 changes: 7 additions & 5 deletions example/awaitable-sender/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
include(FetchContent)

FetchContent_Declare(
execution
GIT_REPOSITORY https://github.com/bemanproject/execution.git
GIT_TAG main
stdexec
GIT_REPOSITORY https://github.com/NVIDIA/stdexec
GIT_TAG 307b83c5689ea7c2e5b31561cdc428697705333e
SYSTEM
FIND_PACKAGE_ARGS
NAMES stdexec
)
FetchContent_MakeAvailable(execution)
FetchContent_MakeAvailable(stdexec)

add_executable(capy_example_awaitable_sender
awaitable_sender.cpp
Expand All @@ -36,7 +38,7 @@ foreach(tgt capy_example_awaitable_sender capy_example_awaitable_sender_test)
target_compile_features(${tgt} PRIVATE cxx_std_23)
target_link_libraries(${tgt}
Boost::capy
beman::execution_headers)
STDEXEC::stdexec)
endforeach()

add_test(NAME capy_example_awaitable_sender_test
Expand Down
35 changes: 29 additions & 6 deletions example/awaitable-sender/awaitable_sender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@

#include <boost/capy.hpp>

#include <beman/execution/execution.hpp>
#include <stdexec/execution.hpp>

#include <chrono>
#include <cstddef>
#include <iostream>
#include <latch>
#include <stop_token>
#include <system_error>
#include <thread>

namespace capy = boost::capy;
namespace ex = beman::execution;
namespace ex = stdexec;

// A receiver whose environment carries a Capy executor.
// Completion signals a latch so main() can wait.
Expand Down Expand Up @@ -187,14 +188,13 @@ int main()
std::cout << "\n--- native awaitable-sender test ---\n";
std::latch done5(1);

auto rop = capy::read_op::result({}, 42);
auto rop = capy::read_op::result({});
auto op5 = ex::connect(
ex::then(
std::move(rop),
[](std::size_t n)
[]
{
std::cout
<< " read " << n << " bytes\n";
std::cout << " read completed\n";
}),
demo_receiver{
{pool_ex, std::stop_token{}},
Expand All @@ -204,6 +204,29 @@ int main()
done5.wait();
std::cout << " native awaitable-sender test done\n";

// A byte-producing op: the count lives in caller-owned state
// (like the buffer it would describe) and is already valid
// when the continuation runs; the completion signal carries
// only the disposition.
std::cout << "\n--- counted read test ---\n";
std::latch done6(1);

std::size_t n = 0;
auto op6 = ex::connect(
ex::then(
capy::counted_read_op::result({}, 42, &n),
[&n]
{
std::cout << " read " << n << " bytes\n";
}),
demo_receiver{
{pool_ex, std::stop_token{}},
&done6});

ex::start(op6);
done6.wait();
std::cout << " counted read test done\n";

// All demos have drained; safe to join the waker thread now.
waker_thread.join();
}
42 changes: 19 additions & 23 deletions example/awaitable-sender/awaitable_sender.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

#include <boost/capy/concept/io_awaitable.hpp>

#include <beman/execution/execution.hpp>
#include <stdexec/execution.hpp>

#include <type_traits>
#include <utility>
Expand Down Expand Up @@ -51,14 +51,6 @@ struct awaitable_sender
return decltype(detail::make_sigs<IoAw>()){};
}

/// beman-compat form: DROP AT GRADUATION (pre-P3164 protocol).
template<class Env>
constexpr auto get_completion_signatures(
Env const&) const noexcept
{
return decltype(detail::make_sigs<IoAw>()){};
}

/// Connect this sender with a receiver to form an operation state.
template<class Receiver>
auto connect(Receiver rcvr) &&
Expand Down Expand Up @@ -87,16 +79,19 @@ struct awaitable_sender
- `error_code` or an empty `io_result` - calls
`set_value()` when the code is zero, `set_error(ec)`
otherwise.
- `io_result<Ts...>` with payload elements - calls
`set_value(ts...)` when `ec` is zero, `set_error(ec)`
otherwise. Any partial payload accompanying a truthy
`ec` is dropped, since sender completion channels are
exclusive.
- 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
`task<error_code>` that inspects the full result, moves the
payload out through a side channel, and returns the code.

For the `error_code`-carrying result types the channel is
chosen by the operation's own disposition: an `ec` that
compares equal to `errc::operation_canceled` completes with
Expand All @@ -118,6 +113,15 @@ struct awaitable_sender
template<class IoAw>
auto as_sender(IoAw&& aw)
{
using R = awaitable_result_t<std::decay_t<IoAw>>;
static_assert(
!detail::is_compound_ec_result_v<R>,
"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<error_code> that inspects the full result "
"and returns the error code.");
return awaitable_sender<std::decay_t<IoAw>>{
std::forward<IoAw>(aw)};
}
Expand Down Expand Up @@ -163,21 +167,13 @@ struct split_ec_sender

Sender sndr_;

// C++26 static-template form plus the beman-compat instance
// form (DROP the latter at graduation; pre-P3164 protocol).
// C++26 static-template form ([exec.getcomplsigs]).
template<class Sndr, class... Env>
static consteval auto get_completion_signatures() noexcept
{
return sigs_type{};
}

template<class Env>
constexpr auto get_completion_signatures(
Env const&) const noexcept
{
return sigs_type{};
}

template<class Receiver>
struct ec_receiver
{
Expand Down
45 changes: 1 addition & 44 deletions example/awaitable-sender/awaitable_sender_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

#include "awaitable_sender_detail.hpp"

#include <type_traits>
#include <utility>

namespace boost::capy {
Expand Down Expand Up @@ -44,28 +43,6 @@ concept AwaitableSender =
IoAwaitable<S> &&
ex::sender<S>;

namespace detail {

// Shared by both connect() overloads so const& connect gets the
// same check as the rvalue overload. Scaffolding-era check, DROP
// AT GRADUATION with the beman-compat query forms: the bundled
// implementation probes decomposable senders by aggregate
// brace-init and hard-errors on arity mismatches. Under the
// adopted wording the durable opt-out is private data members
// (see the class note); aggregates merely take a guarded
// fallback path there.
template<class Derived>
constexpr void check_awaitable_sender()
{
static_assert(
!std::is_aggregate_v<Derived>,
"Derived must not be an aggregate; keep its data "
"members private (or declare a constructor) so "
"sender decomposition cannot claim it");
}

} // namespace detail

/** CRTP mixin that makes an IoAwaitable a sender.

Deriving from this base adds the C++26 sender interface to
Expand Down Expand Up @@ -94,10 +71,7 @@ constexpr void check_awaitable_sender()
first member as a sender tag; private members make the
binding ill-formed, so the op is categorically
non-decomposable and independent of the dispatch
machinery's guarded fallbacks. (`connect` additionally
rejects aggregates; that check accommodates the bundled
pre-standard implementation and is dropped at
graduation.)
machinery's guarded fallbacks.

@par Example
@code
Expand Down Expand Up @@ -129,27 +103,11 @@ struct awaitable_sender_base
return decltype(detail::make_sigs<Derived>()){};
}

/** Return the completion signatures deduced from `Derived`.

beman-compat form: DROP AT GRADUATION. beman::execution
still implements the pre-P3164 protocol and probes for an
instance member called with the environment as a function
argument; the adopted C++26 wording recognizes only the
static template form above.
*/
template<class Env>
constexpr auto get_completion_signatures(
Env const&) const noexcept
{
return decltype(detail::make_sigs<Derived>()){};
}

/// Connect the op to a receiver, consuming it.
template<class Receiver>
auto connect(Receiver rcvr) &&
-> detail::awaitable_op_state<Derived, Receiver>
{
detail::check_awaitable_sender<Derived>();
return detail::awaitable_op_state<Derived, Receiver>(
static_cast<Derived&&>(*this), std::move(rcvr));
}
Expand All @@ -159,7 +117,6 @@ struct awaitable_sender_base
auto connect(Receiver rcvr) const&
-> detail::awaitable_op_state<Derived, Receiver>
{
detail::check_awaitable_sender<Derived>();
return detail::awaitable_op_state<Derived, Receiver>(
static_cast<Derived const&>(*this),
std::move(rcvr));
Expand Down
59 changes: 19 additions & 40 deletions example/awaitable-sender/awaitable_sender_detail.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
#include <boost/capy/ex/io_env.hpp>
#include <boost/capy/io_result.hpp>

#include <beman/execution/execution.hpp>
#include <stdexec/execution.hpp>

#include <concepts>
#include <coroutine>
Expand All @@ -32,7 +32,7 @@ namespace boost::capy {

// On graduation this alias flips to std::execution, gated on
// __cpp_lib_senders.
namespace ex = beman::execution;
namespace ex = stdexec;

// -------------------------------------------------------
// CPO: query a receiver environment for a Capy executor
Expand Down Expand Up @@ -85,9 +85,12 @@ namespace detail {
// 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. The arity trait avoids naming
// std::tuple_size on foreign types, which would hard-error for
// non-tuples (&& does not short-circuit instantiation).
// 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
// types, which would hard-error for non-tuples (&& does not
// short-circuit instantiation).
template<class T>
struct io_result_arity
: std::integral_constant<std::size_t, 0> {};
Expand Down Expand Up @@ -128,18 +131,22 @@ auto concat_sigs(
ex::completion_signatures<B...>)
-> ex::completion_signatures<A..., B...>;

template<class R, std::size_t... I>
auto compound_value_sig(std::index_sequence<I...>)
-> ex::completion_signatures<
ex::set_value_t(std::tuple_element_t<I + 1, R>...),
ex::set_error_t(std::error_code)>;

// Deduce completion signatures from an awaitable's result type.
template<class Aw>
auto make_sigs()
{
using A = std::decay_t<Aw>;
using R = awaitable_result_t<A>;

static_assert(
!is_compound_ec_result_v<R>,
"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<error_code> that inspects the full result "
"and returns the error code.");

constexpr bool nothrow_resume =
noexcept(std::declval<A&>().await_resume());

Expand All @@ -151,10 +158,6 @@ auto make_sigs()
return ex::completion_signatures<
ex::set_value_t(),
ex::set_error_t(std::error_code)>{};
else if constexpr (is_compound_ec_result_v<R>)
return decltype(compound_value_sig<R>(
std::make_index_sequence<
std::tuple_size_v<R> - 1>{})){};
else
return ex::completion_signatures<
ex::set_value_t(R)>{};
Expand All @@ -173,7 +176,7 @@ auto make_sigs()
return decltype(concat_sigs(base, tail)){};
}

// beman's get_stop_token CPO requires the C++26 stoppable_token
// stdexec's get_stop_token CPO requires the C++26 stoppable_token
// concept, which today's std::stop_token fails (no callback_type);
// query the environment directly so std::stop_token environments
// keep cancellation until stdlibs catch up.
Expand Down Expand Up @@ -481,30 +484,6 @@ struct awaitable_op_state
ex::set_error(
std::move(rcvr_), ec);
}
else if constexpr (is_compound_ec_result_v<result_type>)
{
auto result = aw_.await_resume();
auto ec = get<0>(result);
if(ec)
{
if(ec == std::errc::operation_canceled)
ex::set_stopped(
std::move(rcvr_));
else
ex::set_error(
std::move(rcvr_), ec);
}
else
{
[&]<std::size_t... I>(std::index_sequence<I...>)
{
ex::set_value(
std::move(rcvr_),
get<I + 1>(std::move(result))...);
}(std::make_index_sequence<
std::tuple_size_v<result_type> - 1>{});
}
}
else
{
auto result = aw_.await_resume();
Expand Down
Loading
Loading