diff --git a/examples/intro-5-consumer.cpp b/examples/intro-5-consumer.cpp index 0d300a53..9514115e 100644 --- a/examples/intro-5-consumer.cpp +++ b/examples/intro-5-consumer.cpp @@ -128,7 +128,7 @@ struct expected_to_channel_t { sender> operator()(CSender&& child_sender) const { return {std::forward(child_sender)}; } - auto operator()() const { return ex::detail::sender_adaptor{*this}; } + auto operator()() const { return ex::detail::make_sender_adaptor(*this); } }; inline constexpr expected_to_channel_t expected_to_channel{}; diff --git a/include/beman/execution/detail/affine_on.hpp b/include/beman/execution/detail/affine_on.hpp index 789df20a..92c3f95b 100644 --- a/include/beman/execution/detail/affine_on.hpp +++ b/include/beman/execution/detail/affine_on.hpp @@ -31,7 +31,6 @@ import beman.execution.detail.schedule; import beman.execution.detail.schedule_from; import beman.execution.detail.scheduler; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.sender_for; import beman.execution.detail.sender_has_affine_on; @@ -53,7 +52,6 @@ import beman.execution.detail.write_env; #include #include #include -#include #include #include #include @@ -108,7 +106,7 @@ struct affine_on_t : ::beman::execution::sender_adaptor_closure { * * @return A sender adaptor for the affine_on_t. */ - auto operator()() const { return ::beman::execution::detail::sender_adaptor{*this}; } + auto operator()() const { return ::beman::execution::detail::make_sender_adaptor(*this); } /** * @brief affine_on is implemented by transforming it into a use of schedule_from. diff --git a/include/beman/execution/detail/associate.hpp b/include/beman/execution/detail/associate.hpp index 6ae90a1d..8b567d01 100644 --- a/include/beman/execution/detail/associate.hpp +++ b/include/beman/execution/detail/associate.hpp @@ -28,7 +28,7 @@ import beman.execution.detail.make_sender; import beman.execution.detail.nothrow_callable; import beman.execution.detail.scope_token; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; +import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.set_stopped; import beman.execution.detail.set_value; import beman.execution.detail.start; @@ -118,7 +118,7 @@ struct associate_t { template <::beman::execution::scope_token Token> auto operator()(Token token) const { - return ::beman::execution::detail::sender_adaptor{*this, ::std::move(token)}; + return ::beman::execution::detail::make_sender_adaptor(*this, ::std::move(token)); } public: diff --git a/include/beman/execution/detail/bulk.hpp b/include/beman/execution/detail/bulk.hpp index a7b37922..ace7030d 100644 --- a/include/beman/execution/detail/bulk.hpp +++ b/include/beman/execution/detail/bulk.hpp @@ -28,7 +28,6 @@ import beman.execution.detail.meta.unique; import beman.execution.detail.movable_value; import beman.execution.detail.product_type; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.set_error; import beman.execution.detail.set_value; @@ -84,7 +83,8 @@ struct bulk_t : ::beman::execution::sender_adaptor_closure { template requires(std::is_integral_v && ::beman::execution::detail::movable_value) auto operator()(Shape&& shape, f&& fun) const { - return beman::execution::detail::sender_adaptor{*this, std::forward(shape), std::forward(fun)}; + return ::beman::execution::detail::make_sender_adaptor( + *this, std::forward(shape), std::forward(fun)); } template diff --git a/include/beman/execution/detail/common.hpp b/include/beman/execution/detail/common.hpp index 94db69b5..77235773 100644 --- a/include/beman/execution/detail/common.hpp +++ b/include/beman/execution/detail/common.hpp @@ -59,7 +59,20 @@ namespace execution { * \brief Namespace for implementation details related to beman::execution * \internal */ -namespace detail {} +namespace detail { + +/*! + * \namespace beman::execution::detail::pipeable + * \brief Namespace for ADL isolation of sender adaptor closure pipe operators. + * + * \details + * The operator| overloads for sender adaptor closures are placed in this + * namespace so they are only found via argument-dependent lookup when one + * of the arguments derives from sender_adaptor_closure. + * \internal + */ +namespace pipeable {} +} // namespace detail } // namespace execution } // namespace beman diff --git a/include/beman/execution/detail/continues_on.hpp b/include/beman/execution/detail/continues_on.hpp index 9bcb0164..354df68f 100644 --- a/include/beman/execution/detail/continues_on.hpp +++ b/include/beman/execution/detail/continues_on.hpp @@ -27,7 +27,7 @@ import beman.execution.detail.sched_attrs; import beman.execution.detail.schedule_from; import beman.execution.detail.scheduler; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; +import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.sender_for; import beman.execution.detail.transform_sender; #else @@ -70,7 +70,7 @@ struct continues_on_t { } template <::beman::execution::scheduler Scheduler> auto operator()(Scheduler&& scheduler) const { - return ::beman::execution::detail::sender_adaptor{*this, ::std::forward(scheduler)}; + return ::beman::execution::detail::make_sender_adaptor(*this, ::std::forward(scheduler)); } template <::beman::execution::sender Sender, ::beman::execution::scheduler Scheduler> auto operator()(Sender&& sender, Scheduler&& scheduler) const { diff --git a/include/beman/execution/detail/let.hpp b/include/beman/execution/detail/let.hpp index fc7c1c69..7d5da39a 100644 --- a/include/beman/execution/detail/let.hpp +++ b/include/beman/execution/detail/let.hpp @@ -49,7 +49,7 @@ import beman.execution.detail.movable_value; import beman.execution.detail.receiver; import beman.execution.detail.sched_env; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; +import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.set_error; import beman.execution.detail.set_stopped; import beman.execution.detail.set_value; @@ -99,7 +99,7 @@ template struct let_t { template <::beman::execution::detail::movable_value Fun> auto operator()(Fun&& fun) const { - return ::beman::execution::detail::sender_adaptor{*this, ::std::forward(fun)}; + return ::beman::execution::detail::make_sender_adaptor(*this, ::std::forward(fun)); } template <::beman::execution::sender Sender, ::beman::execution::detail::movable_value Fun> auto operator()(Sender&& sender, Fun&& fun) const { diff --git a/include/beman/execution/detail/on.hpp b/include/beman/execution/detail/on.hpp index 6c31943f..ce12ead7 100644 --- a/include/beman/execution/detail/on.hpp +++ b/include/beman/execution/detail/on.hpp @@ -27,7 +27,6 @@ import beman.execution.detail.query_with_default; import beman.execution.detail.sched_env; import beman.execution.detail.scheduler; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.sender_for; import beman.execution.detail.set_value; @@ -157,8 +156,8 @@ struct on_t : ::beman::execution::sender_adaptor_closure { } template <::beman::execution::scheduler Sch, ::beman::execution::detail::is_sender_adaptor_closure Closure> auto operator()(Sch&& sch, Closure&& closure) const { - return ::beman::execution::detail::sender_adaptor{ - *this, ::std::forward(sch), ::std::forward(closure)}; + return ::beman::execution::detail::make_sender_adaptor( + *this, ::std::forward(sch), ::std::forward(closure)); } }; diff --git a/include/beman/execution/detail/sender_adaptor.hpp b/include/beman/execution/detail/sender_adaptor.hpp index f0b2e845..e7f721cb 100644 --- a/include/beman/execution/detail/sender_adaptor.hpp +++ b/include/beman/execution/detail/sender_adaptor.hpp @@ -28,28 +28,14 @@ import beman.execution.detail.sender_decompose; // ---------------------------------------------------------------------------- namespace beman::execution::detail { -template //-dk:TODO detail export -struct sender_adaptor : ::beman::execution::detail::product_type<::std::decay_t, ::std::decay_t...>, - ::beman::execution::sender_adaptor_closure> { - template <::beman::execution::sender Sender, typename Self> - static auto apply(Sender&& sender, Self&& self) { - return [&self, &sender]<::std::size_t... I>(::std::index_sequence) { - auto&& fun(self.template get<0>()); - return fun(::std::forward(sender), - ::beman::execution::detail::forward_like(self.template get())...); - }(::std::make_index_sequence{}); - } - template <::beman::execution::sender Sender> - auto operator()(Sender&& sender) { - return apply(::std::forward(sender), ::std::move(*this)); - } - template <::beman::execution::sender Sender> - auto operator()(Sender&& sender) const { - return apply(::std::forward(sender), *this); - } -}; + template -sender_adaptor(T&&...) -> sender_adaptor; +using sender_adaptor + [[deprecated("sender_adaptor is deprecated and layout incompatible with previous versions." + " Use make_sender_adaptor(adaptor, args...) instead. " + "The implementation now uses bound_sender_adaptor_closure, which stores the adaptor with " + "[[no_unique_address]] and keeps bound arguments in product_type.")]] = + bound_sender_adaptor_closure...>; } // namespace beman::execution::detail // ---------------------------------------------------------------------------- diff --git a/include/beman/execution/detail/sender_adaptor_closure.hpp b/include/beman/execution/detail/sender_adaptor_closure.hpp index 6898d16a..457fea64 100644 --- a/include/beman/execution/detail/sender_adaptor_closure.hpp +++ b/include/beman/execution/detail/sender_adaptor_closure.hpp @@ -14,41 +14,188 @@ import std; #endif #ifdef BEMAN_HAS_MODULES import beman.execution.detail.sender; +import beman.execution.detail.call_result_t; +import beman.execution.detail.callable; +import beman.execution.detail.class_type; +import beman.execution.detail.nothrow_callable; +import beman.execution.detail.movable_value; +import beman.execution.detail.product_type; + #else #include +#include +#include +#include +#include +#include +#include + #endif // ---------------------------------------------------------------------------- namespace beman::execution::detail::pipeable { -struct sender_adaptor_closure_base {}; +/*! + * \brief ADL anchor tag type inherited by sender_adaptor_closure. + * \headerfile beman/execution/execution.hpp + * \internal + */ +struct closure_t {}; } // namespace beman::execution::detail::pipeable namespace beman::execution { -// NOLINTBEGIN(bugprone-crtp-constructor-accessibility) -template -struct sender_adaptor_closure : ::beman::execution::detail::pipeable::sender_adaptor_closure_base {}; -// NOLINTEND(bugprone-crtp-constructor-accessibility) +/*! + * \brief CRTP base class for pipeable sender adaptor closure objects. + * \headerfile beman/execution/execution.hpp + */ +template +struct sender_adaptor_closure : detail::pipeable::closure_t {}; } // namespace beman::execution namespace beman::execution::detail { -template + +/*! + * \brief Helper to detect a unique sender_adaptor_closure base class. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template +auto get_sender_adaptor_closure_base(const sender_adaptor_closure&) -> T; + +/*! + * \brief Checks that T has exactly one sender_adaptor_closure base where U == decay_t. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template +concept has_unique_sender_adaptor_closure_base = requires(const T& s) { + { get_sender_adaptor_closure_base(s) } -> std::same_as>; +}; + +/*! + * \brief Determine if a type is a pipeable sender adaptor closure. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template concept is_sender_adaptor_closure = - ::std::derived_from<::std::decay_t, ::beman::execution::sender_adaptor_closure<::std::decay_t>>; + std::derived_from, sender_adaptor_closure>> and + has_unique_sender_adaptor_closure_base> and (not sender>); + +/*! + * \brief Checks that Closure is a pipeable sender adaptor closure invocable with Sender. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template +concept sender_adaptor_closure_for = + is_sender_adaptor_closure and sender and requires(Closure&& closure, Sender&& sndr) { + { std::forward(closure)(std::forward(sndr)) } -> sender; + }; + +/*! + * \brief Utility alias to copy cv-ref qualifiers from one type onto another. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template +using apply_cvref_t = decltype(std::forward_like(std::declval())); + +/*! + * \brief Perfect forwarding call wrapper produced by closure-closure composition via operator|. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template +struct composed_sender_adaptor_closure : sender_adaptor_closure> { + [[no_unique_address]] Inner inner; + [[no_unique_address]] Outer outer; + + template + requires callable, Sender> and + callable, call_result_t, Sender>> + constexpr auto operator()(this Self&& self, Sender&& sndr) noexcept( + nothrow_callable, Sender> and + nothrow_callable, call_result_t, Sender>>) + -> call_result_t, call_result_t, Sender>> { + return std::forward_like(self.outer)(std::forward_like(self.inner)(std::forward(sndr))); + } +}; + +// ctad +template +composed_sender_adaptor_closure(Inner&&, Outer&&) + -> composed_sender_adaptor_closure, std::decay_t>; + +/*! + * \brief Perfect forwarding call wrapper produced by adaptor(args...) for multi-argument adaptors. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template +struct bound_sender_adaptor_closure : detail::product_type...>, + sender_adaptor_closure> { + + [[no_unique_address]] Adaptor adaptor; + + template + requires callable, Sender, apply_cvref_t...> + constexpr auto operator()(this Self&& self, Sender&& sndr) noexcept( + nothrow_callable, Sender, apply_cvref_t...>) + -> call_result_t, Sender, apply_cvref_t...> { + return self.apply([&](auto&&... bound_args) { + return std::forward_like(self.adaptor)(std::forward(sndr), + std::forward_like(bound_args)...); + }); + } +}; + +template +bound_sender_adaptor_closure(Tag&&, Args&&...) + -> bound_sender_adaptor_closure, std::decay_t...>; + +/*! + * \brief Factory function producing a bound_sender_adaptor_closure from an adaptor and arguments. + * \headerfile beman/execution/execution.hpp + * \internal + */ +template + requires(movable_value && ...) +constexpr auto +make_sender_adaptor(Tag&& tag, + Args&&... args) noexcept(std::is_nothrow_constructible_v, Tag> and + (std::is_nothrow_constructible_v, Args> and ...)) + -> bound_sender_adaptor_closure, std::decay_t...> { + return {{std::forward(args)...}, {}, tag}; } +} // namespace beman::execution::detail namespace beman::execution::detail::pipeable { -template <::beman::execution::sender Sender, typename Adaptor> - requires(!::beman::execution::sender) && - ::std::derived_from<::std::decay_t, - ::beman::execution::sender_adaptor_closure<::std::decay_t>> && - requires(Sender&& sender, Adaptor&& adaptor) { - { adaptor(::std::forward(sender)) } -> ::beman::execution::sender; - } -auto operator|(Sender&& sender, Adaptor&& adaptor) { - return adaptor(::std::forward(sender)); + +/*! + * \brief Pipe operator connecting a sender to a pipeable sender adaptor closure. + * \headerfile beman/execution/execution.hpp + */ +template Closure> +constexpr auto operator|(Sender&& sndr, Closure&& cl) noexcept(detail::nothrow_callable) + -> detail::call_result_t { + return std::forward(cl)(std::forward(sndr)); +} + +/*! + * \brief Pipe operator composing two pipeable sender adaptor closure objects. + * \headerfile beman/execution/execution.hpp + */ +template + requires std::constructible_from, Inner> && std::constructible_from, Outer> +constexpr auto operator|(Inner&& inner, + Outer&& outer) noexcept(std::is_nothrow_constructible_v, Inner> && + std::is_nothrow_constructible_v, Outer>) + -> detail::composed_sender_adaptor_closure, std::decay_t> { + return {{}, std::forward(inner), std::forward(outer)}; } + } // namespace beman::execution::detail::pipeable // ---------------------------------------------------------------------------- diff --git a/include/beman/execution/detail/then.hpp b/include/beman/execution/detail/then.hpp index 95315430..36b1d992 100644 --- a/include/beman/execution/detail/then.hpp +++ b/include/beman/execution/detail/then.hpp @@ -30,7 +30,6 @@ import beman.execution.detail.meta.unique; import beman.execution.detail.movable_value; import beman.execution.detail.nested_sender_has_affine_on; import beman.execution.detail.sender; -import beman.execution.detail.sender_adaptor; import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.set_error; import beman.execution.detail.set_stopped; @@ -108,7 +107,7 @@ template struct then_t : ::beman::execution::sender_adaptor_closure> { template <::beman::execution::detail::movable_value Fun> auto operator()(Fun&& fun) const { - return ::beman::execution::detail::sender_adaptor{*this, ::std::forward(fun)}; + return ::beman::execution::detail::make_sender_adaptor(*this, std::forward(fun)); } template <::beman::execution::sender Sender, ::beman::execution::detail::movable_value Fun> auto operator()(Sender&& sender, Fun&& fun) const { diff --git a/include/beman/execution/execution.hpp b/include/beman/execution/execution.hpp index ca8d9f41..b0225977 100644 --- a/include/beman/execution/execution.hpp +++ b/include/beman/execution/execution.hpp @@ -40,6 +40,7 @@ import beman.execution.detail.schedule_from; import beman.execution.detail.schedule; import beman.execution.detail.scheduler; import beman.execution.detail.scope_token; +import beman.execution.detail.sender_adaptor_closure; import beman.execution.detail.sender_in; import beman.execution.detail.sender; import beman.execution.detail.set_error; @@ -92,6 +93,7 @@ import beman.execution.detail.write_env; #include #include #include +#include #include #include #include diff --git a/src/beman/execution/execution-detail.cppm b/src/beman/execution/execution-detail.cppm index 79de86aa..568be1bb 100644 --- a/src/beman/execution/execution-detail.cppm +++ b/src/beman/execution/execution-detail.cppm @@ -65,7 +65,6 @@ export import beman.execution.detail.queryable; export import beman.execution.detail.sched_attrs; export import beman.execution.detail.sched_env; export import beman.execution.detail.sender; -export import beman.execution.detail.sender_adaptor; export import beman.execution.detail.sender_adaptor_closure; export import beman.execution.detail.sender_decompose; export import beman.execution.detail.sender_for; diff --git a/src/beman/execution/execution.cppm b/src/beman/execution/execution.cppm index e738c711..8a2c8fe1 100644 --- a/src/beman/execution/execution.cppm +++ b/src/beman/execution/execution.cppm @@ -91,7 +91,6 @@ export import beman.execution.detail.sender_in; export import beman.execution.detail.sends_stopped; namespace beman::execution { -export using ::beman::execution::operator|; export using ::beman::execution::nostopstate_t; export using ::beman::execution::nostopstate; diff --git a/src/beman/execution/sender_adaptor_closure.cppm b/src/beman/execution/sender_adaptor_closure.cppm index 348a7929..a514ce64 100644 --- a/src/beman/execution/sender_adaptor_closure.cppm +++ b/src/beman/execution/sender_adaptor_closure.cppm @@ -6,12 +6,21 @@ module; export module beman.execution.detail.sender_adaptor_closure; -namespace beman::execution { -export using beman::execution::detail::pipeable::operator|; -} +namespace beman::execution::detail::pipeable { +export using ::beman::execution::detail::pipeable::closure_t; +export using ::beman::execution::detail::pipeable::operator|; +} // namespace beman::execution::detail::pipeable namespace beman::execution::detail { -export using beman::execution::detail::is_sender_adaptor_closure; -} +export using ::beman::execution::detail::is_sender_adaptor_closure; +export using ::beman::execution::detail::sender_adaptor_closure_for; +export using ::beman::execution::detail::get_sender_adaptor_closure_base; +export using ::beman::execution::detail::apply_cvref_t; +export using ::beman::execution::detail::composed_sender_adaptor_closure; +export using ::beman::execution::detail::bound_sender_adaptor_closure; +export using ::beman::execution::detail::make_sender_adaptor; + +} // namespace beman::execution::detail namespace beman::execution { -export using beman::execution::sender_adaptor_closure; +export using ::beman::execution::sender_adaptor_closure; + } // namespace beman::execution diff --git a/tests/beman/execution/CMakeLists.txt b/tests/beman/execution/CMakeLists.txt index 29f18f2f..4c2ea0e3 100644 --- a/tests/beman/execution/CMakeLists.txt +++ b/tests/beman/execution/CMakeLists.txt @@ -46,6 +46,7 @@ list( exec-scope-concepts.test exec-scope-counting.test exec-scope-simple-counting.test + exec-sender-adaptor-closure.test exec-set-error.test exec-set-stopped.test exec-set-value.test diff --git a/tests/beman/execution/exec-on.test.cpp b/tests/beman/execution/exec-on.test.cpp index 5c3ee4c9..ad3bbefb 100644 --- a/tests/beman/execution/exec-on.test.cpp +++ b/tests/beman/execution/exec-on.test.cpp @@ -24,30 +24,22 @@ import beman.execution.detail; // ---------------------------------------------------------------------------- namespace { -struct both : test_std::sender_adaptor_closure { - using sender_concept = test_std::sender_t; -}; - -static_assert(test_std::sender); -static_assert(test_detail::is_sender_adaptor_closure); - -template -auto test_interface(Sch sch, Sndr sndr, Closure closure, Both both) -> void { +template +auto test_interface(Sch sch, Sndr sndr, Closure closure) -> void { static_assert(requires { { test_std::on(sch, sndr) } -> test_std::sender; }); - static_assert(not requires { test_std::on(sch, both); }); static_assert(requires { { test_std::on(sndr, sch, closure) } -> test_std::sender; }); - static_assert(not requires { test_std::on(both, sch, closure); }); + static_assert(requires { + { test_std::on(sch, closure) } -> test_detail::is_sender_adaptor_closure; + }); auto sndr1{test_std::on(sch, sndr)}; auto sndr2{test_std::on(sndr, sch, closure)}; - test::use(sndr1, sndr2); + auto sndr3{test_std::on(sch, closure)}; + test::use(sndr1, sndr2, sndr3); } template OutSndr> @@ -81,7 +73,7 @@ TEST(exec_on) { static_assert(std::same_as); static_assert(test_detail::is_sender_adaptor_closure); static_assert(not test_detail::is_sender_adaptor_closure); - test_interface(pool.get_scheduler(), test_std::just(), test_std::then([] {}), both{}); + test_interface(pool.get_scheduler(), test_std::just(), test_std::then([] {})); test_transform_env(test_detail::make_sender(test_std::on, pool.get_scheduler(), test_std::just())); test_transform_env(test_detail::make_sender( diff --git a/tests/beman/execution/exec-sender-adaptor-closure.test.cpp b/tests/beman/execution/exec-sender-adaptor-closure.test.cpp new file mode 100644 index 00000000..a69b873f --- /dev/null +++ b/tests/beman/execution/exec-sender-adaptor-closure.test.cpp @@ -0,0 +1,327 @@ +// tests/beman/execution/exec-sender-adaptor-closure.test.cpp -*-C++-*- +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include + +#include + +#ifdef BEMAN_HAS_MODULES +import beman.execution; +import beman.execution.detail; +#else +#include +#endif + +namespace { + +// test helpers +template +struct wrapping_sender { + using sender_concept = test_std::sender_t; + Sender inner; + + template + static auto connect(Self&& self, Rcvr&& rcvr) { + return test_std::connect(std::forward(self).inner, std::forward(rcvr)); + } + + template > + using completion_signatures = test_std::completion_signatures_of_t; +}; +struct wrapping_closure : test_std::sender_adaptor_closure { + template + auto operator()(Sender&& sndr) const { + return wrapping_sender>{std::forward(sndr)}; + } +}; +struct identity_closure : test_std::sender_adaptor_closure { + template + auto operator()(Sender&& sndr) const { + return std::forward(sndr); + } +}; + +struct adaptor_cpo : test_std::sender_adaptor_closure { + auto operator()(auto&&... vals) const { return test_detail::make_sender_adaptor(*this, vals...); } + + template + auto operator()(Sender&& sndr, auto&&... vals) const { + return test_detail::make_sender( + *this, test_detail::product_type{::std::forward(vals)...}, ::std::forward(sndr)); + } +}; + +struct wrong_crtp_closure : test_std::sender_adaptor_closure {}; +struct double_inheritance_closure : identity_closure, test_std::sender_adaptor_closure {}; +struct extended_closure : identity_closure {}; +struct both_sender_and_closure : test_std::sender_adaptor_closure { + using sender_concept = test_std::sender_t; +}; + +struct incomplete; +using incomplete_base = test_std::sender_adaptor_closure; +struct incomplete : incomplete_base {}; +struct nothrow_closure : test_std::sender_adaptor_closure { + template + auto operator()(Sender&& sndr) const noexcept { + return std::forward(sndr); + } +}; + +struct move_only_closure : test_std::sender_adaptor_closure { + move_only_closure() = default; + move_only_closure(move_only_closure&&) = default; + move_only_closure(const move_only_closure&) = delete; + move_only_closure& operator=(move_only_closure&&) = default; + move_only_closure& operator=(const move_only_closure&) = delete; + + template + auto operator()(Sender&& sndr) const { + return std::forward(sndr); + } +}; + +struct lvalue_rvalue_closure : test_std::sender_adaptor_closure { + template + auto operator()(Sender&& sndr) & { + (void)sndr; + return test_std::just(1); // lvalue result + } + + template + auto operator()(Sender&& sndr) && { + (void)sndr; + return test_std::just(2.0); // rvalue result + } +}; + +struct minimal_sender : decltype(test_std::just()){}; + +struct constrained_closure : test_std::sender_adaptor_closure { + template + requires std::same_as, minimal_sender> + auto operator()(Sender&& sndr) const { + return std::forward(sndr); + } +}; + +struct non_composable_closure : test_std::sender_adaptor_closure { + non_composable_closure() = default; + non_composable_closure(const non_composable_closure&) = delete; + non_composable_closure(non_composable_closure&&) = delete; + non_composable_closure& operator=(const non_composable_closure&) = delete; + non_composable_closure& operator=(non_composable_closure&&) = delete; +}; + +// helper concepts +template +concept can_compose_closures = requires(Closure1 c1, Closure2 c2) { c1 | c2; }; + +template +concept can_make_adaptor = + requires(Tag tag, Args... args) { test_detail::make_sender_adaptor(std::move(tag), std::move(args)...); }; + +struct non_movable { + non_movable() = default; + non_movable(const non_movable&) = delete; + non_movable(non_movable&&) = delete; + non_movable& operator=(const non_movable&) = delete; + non_movable& operator=(non_movable&&) = delete; +}; + +template +concept can_pipe = requires(Sender sndr, Closure closure) { sndr | closure; }; + +// test functions +auto test_basic_closure_validity() -> void { + static_assert(test_detail::is_sender_adaptor_closure, "identity_closure should pass"); + static_assert(not test_detail::is_sender_adaptor_closure, "wrong CRTP should fail"); + static_assert(not test_detail::is_sender_adaptor_closure, + "non-unique base should fail"); + static_assert(not test_detail::is_sender_adaptor_closure, "no direct CRTP should fail"); + static_assert(not test_detail::is_sender_adaptor_closure, + "closure cannot also model sender"); + static_assert(test_detail::is_sender_adaptor_closure, + "incomplete CRTP base should work once completed"); +} + +auto test_basic_pipe_syntax() -> void { + auto sndr = test_std::just(1); + auto adapted = sndr | wrapping_closure{}; + static_assert(std::same_as>); +} + +auto test_composition_syntax() -> void { + static_assert(can_compose_closures); + auto clos1 = wrapping_closure{}; + auto clos2 = wrapping_closure{}; + auto composed = clos1 | clos2; + + static_assert(test_detail::is_sender_adaptor_closure); + + // snd | composed + auto sndr = test_std::just(1); + auto adapted = sndr | composed; + + // should be wrapped twice: wrapper> + using expected_t = wrapping_sender>; + static_assert(std::same_as); +} + +auto test_associativity() -> void { + // (snd | c1) | c2 == snd | (c1 | c2) + auto sndr = test_std::just(1); + auto clos1 = wrapping_closure{}; + auto clos2 = wrapping_closure{}; + + auto res1 = (sndr | clos1) | clos2; + auto res2 = sndr | (clos1 | clos2); + + static_assert(std::same_as); +} + +auto test_pipe_equivalence() -> void { + auto sndr = test_std::just(1); + using left_t = decltype(wrapping_closure{}(sndr)); + using pipe_t = decltype(sndr | wrapping_closure{}); + static_assert(std::same_as); +} + +auto test_partial_application() -> void { + auto closure = adaptor_cpo{}(1995); + static_assert(test_detail::is_sender_adaptor_closure); + + auto sndr = test_std::just(1); + auto direct = closure(sndr); + auto piped = sndr | closure; + static_assert(std::same_as); + static_assert(test_std::sender); +} + +auto test_noexcept_propagation() -> void { + auto sndr = test_std::just(1); + + // nothrow closure => pipe is noexcept + static_assert(noexcept(sndr | nothrow_closure{})); + static_assert(noexcept(nothrow_closure{}(sndr))); + + // non-noexcept closure => pipe is not noexcept + static_assert(not noexcept(sndr | identity_closure{} | nothrow_closure{})); + static_assert(not noexcept(identity_closure{}(sndr))); + + // composition + static_assert(noexcept(nothrow_closure{} | nothrow_closure{})); +} + +auto test_composed_call_pattern() -> void { + auto sndr = test_std::just(1); + + // composed(sndr) should wrap twice: wrapping_sender> + auto composed = wrapping_closure{} | wrapping_closure{}; + auto result = composed(sndr); + + using inner_t = wrapping_sender>; + using expected_t = wrapping_sender; + static_assert(std::same_as); + + // multi-level: c(b(a(arg))) + auto abc = wrapping_closure{} | wrapping_closure{} | wrapping_closure{}; + auto result_abc = sndr | abc; + using middle_t = wrapping_sender; + using expected_abc_t = wrapping_sender; + static_assert(std::same_as); +} + +auto test_partial_application_well_formedness() -> void { + // make_adaptor with a non-movable arg should be ill-formed (SFINAE) + static_assert(not can_make_adaptor); + + // make_adaptor with a movable arg should be well-formed + static_assert(requires(adaptor_cpo cpo) { test_detail::make_sender_adaptor(cpo, 999); }); +} + +auto test_move_only_closure() -> void { + auto sndr = test_std::just(1); + + // direct + auto c1 = move_only_closure{}; + (void)std::move(c1)(sndr); + + // pipe + auto c2 = move_only_closure{}; + (void)(sndr | std::move(c2)); + + // composition + auto composed = move_only_closure{} | move_only_closure{}; + (void)(sndr | std::move(composed)); +} + +auto test_ref_qualification_propagation() -> void { + auto sndr = test_std::just(1); + + // direct application + auto c1 = lvalue_rvalue_closure{}; + static_assert(std::same_as); + static_assert(std::same_as); + + // pipe application + auto c2 = lvalue_rvalue_closure{}; + static_assert(std::same_as); + static_assert(std::same_as); + + // composition + auto composed_l = lvalue_rvalue_closure{} | identity_closure{}; + static_assert(std::same_as); + + auto composed_r = lvalue_rvalue_closure{} | identity_closure{}; + static_assert(std::same_as); +} + +auto test_multi_arg_adaptor() -> void { + auto sndr = test_std::just(); + auto closure = adaptor_cpo{}(42, 3.14); + + static_assert(test_detail::is_sender_adaptor_closure); + + auto res = sndr | closure; + (void)res; + + // Check movable arguments + auto closure_moved = adaptor_cpo{}(42, 3.14); + (void)(sndr | std::move(closure_moved)); +} + +auto test_sfinae_friendliness() -> void { + // constrained_closure only accepts minimal_sender + static_assert(can_pipe); + static_assert(not can_pipe); + + // composition SFINAE: (constrained | identity)(just()) should fail + static_assert(not can_pipe); +} + +auto test_composition_well_formedness() -> void { + // non-copyable/non-movable closure cannot compose (spec ยง1: well-formed if initializations well-formed) + static_assert(not can_compose_closures); + static_assert(not can_compose_closures); + static_assert(not can_compose_closures); +} + +} // namespace + +TEST(exec_sender_adaptor_closure) { + test_basic_closure_validity(); + test_basic_pipe_syntax(); + test_composition_syntax(); + test_associativity(); + test_pipe_equivalence(); + test_partial_application(); + test_noexcept_propagation(); + test_composed_call_pattern(); + test_partial_application_well_formedness(); + test_move_only_closure(); + test_ref_qualification_propagation(); + test_multi_arg_adaptor(); + test_sfinae_friendliness(); + test_composition_well_formedness(); +} diff --git a/tests/beman/execution/execution-syn.test.cpp b/tests/beman/execution/execution-syn.test.cpp index 4755ccac..b957f37c 100644 --- a/tests/beman/execution/execution-syn.test.cpp +++ b/tests/beman/execution/execution-syn.test.cpp @@ -303,7 +303,7 @@ struct closure_t : test_std::sender_adaptor_closure { constexpr closure_t closure{}; auto test_sender_adaptor_closure() -> void { - use(test_std::sender_adaptor_closure{}); + use(test_std::sender_adaptor_closure{}); struct sender { using sender_concept = test_std::sender_t; }; @@ -325,7 +325,7 @@ struct arg_closure_t { auto operator()(Sender&&, int) const -> adapted_sender { return {}; } - auto operator()(int value) const { return test_detail::sender_adaptor{*this, value, {}}; } + auto operator()(int value) const { return test_detail::make_sender_adaptor(*this, value); } }; constexpr arg_closure_t arg_closure{}; @@ -336,7 +336,7 @@ auto test_sender_adaptor() -> void { static_assert(test_std::sender); auto closure_{arg_closure(17)}; - static_assert(std::same_as, decltype(closure_)>); + static_assert(std::same_as, decltype(closure_)>); auto direct{closure_(sender{})}; test::use(direct); static_assert(std::same_as, decltype(direct)>);