diff --git a/include/stdexec/__detail/__associate.hpp b/include/stdexec/__detail/__associate.hpp new file mode 100644 index 000000000..810f2fcf3 --- /dev/null +++ b/include/stdexec/__detail/__associate.hpp @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2025 Ian Petersen + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "__execution_fwd.hpp" + +#include "__basic_sender.hpp" +#include "__concepts.hpp" +#include "__diagnostics.hpp" +#include "__queries.hpp" +#include "__scope_concepts.hpp" +#include "__senders.hpp" +#include "__sender_adaptor_closure.hpp" + +namespace stdexec { + ///////////////////////////////////////////////////////////////////////////// + // [exec.associate] + namespace __associate { + template + struct __associate_data { + using __wrap_result_t = decltype(__declval<_Token&>().wrap(__declval<_Sender>())); + using __wrap_sender_t = std::remove_cvref_t<__wrap_result_t>; + + using __assoc_t = decltype(__declval<_Token&>().try_associate()); + + using __sender_ref = + std::unique_ptr<__wrap_sender_t, decltype([](auto* p) noexcept { std::destroy_at(p); })>; + + // BUGBUG: should the spec require __token to be declared as a const _Token, or should this be + // changed to declare __token as a mutable _Token? + explicit __associate_data(const _Token __token, _Sender&& __sndr) noexcept( + __nothrow_constructible_from<__wrap_sender_t, __wrap_result_t> + && noexcept(__token.wrap(static_cast<_Sender&&>(__sndr))) + && noexcept(__token.try_associate())) + : __sndr_(__token.wrap(static_cast<_Sender&&>(__sndr))) + , __assoc_([&] { + __sender_ref guard{std::addressof(__sndr_)}; + + auto assoc = __token.try_associate(); + + if (assoc) { + (void) guard.release(); + } + + return assoc; + }()) { + } + + __associate_data(const __associate_data& __other) noexcept( + __nothrow_copy_constructible<__wrap_sender_t> && noexcept(__other.__assoc_.try_associate())) + requires copy_constructible<__wrap_sender_t> + : __assoc_(__other.__assoc_.try_associate()) { + if (__assoc_) { + std::construct_at(&__sndr_, __other.__sndr_); + } + } + + __associate_data(__associate_data&& __other) + noexcept(__nothrow_move_constructible<__wrap_sender_t>) + : __associate_data(std::move(__other).release()) { + } + + ~__associate_data() { + if (__assoc_) { + std::destroy_at(&__sndr_); + } + } + + std::pair<__assoc_t, __sender_ref> release() && noexcept { + __sender_ref u(__assoc_ ? std::addressof(__sndr_) : nullptr); + return {std::move(__assoc_), std::move(u)}; + } + + private: + __associate_data(std::pair<__assoc_t, __sender_ref> __parts) + : __assoc_(std::move(__parts.first)) { + if (__assoc_) { + std::construct_at(&__sndr_, std::move(*__parts.second)); + } + } + + union { + __wrap_sender_t __sndr_; + }; + __assoc_t __assoc_; + }; + + template + __associate_data(_Token, _Sender&&) -> __associate_data<_Token, _Sender>; + + //////////////////////////////////////////////////////////////////////////////////////////////// + struct associate_t { + template + auto operator()(_Sender&& __sndr, _Token&& __token) const + noexcept(__nothrow_constructible_from< + __associate_data, _Sender>, + _Token, + _Sender + >) -> __well_formed_sender auto { + return __make_sexpr( + __associate_data(static_cast<_Token&&>(__token), static_cast<_Sender&&>(__sndr))); + } + + template + STDEXEC_ATTRIBUTE(always_inline) + auto operator()(_Token&& __token) const noexcept { + return __closure(*this, static_cast<_Token&&>(__token)); + } + }; + + struct __associate_impl : __sexpr_defaults { + static constexpr auto get_attrs = [](__ignore, const _Child& __child) noexcept { + return __sync_attrs{__child}; + }; + + static constexpr auto get_completion_signatures = + [](_Sender&&, _Env&&...) noexcept + -> transform_completion_signatures< + __completion_signatures_of_t::__wrap_sender_t>, + completion_signatures + > { + static_assert(sender_expr_for<_Sender, associate_t>); + return {}; + }; + + static constexpr auto get_state = + [](_Self&& __self, _Receiver& __rcvr) noexcept( + (same_as<_Self, std::remove_cvref_t<_Self>> + || __nothrow_constructible_from, _Self>) && + __nothrow_callable< + connect_t, + typename std::remove_cvref_t<__data_of<_Self>>::__wrap_sender_t, + _Receiver + >) { + auto&& [_, data] = std::forward<_Self>(__self); + + using associate_data_t = std::remove_cvref_t; + using assoc_t = associate_data_t::__assoc_t; + using sender_ref_t = associate_data_t::__sender_ref; + + using op_t = connect_result_t; + + struct op_state { + assoc_t __assoc_; + union { + _Receiver* __rcvr_; + op_t __op_; + }; + + explicit op_state(std::pair parts, _Receiver r) + : __assoc_(std::move(parts.first)) { + if (__assoc_) { + ::new ((void*) std::addressof(__op_)) + op_t(connect(std::move(*parts.second), std::move(r))); + } else { + __rcvr_ = std::addressof(r); + } + } + + explicit op_state(associate_data_t&& ad, _Receiver& r) + : op_state(std::move(ad).release(), r) { + } + + explicit op_state(const associate_data_t& ad, _Receiver& r) + requires copy_constructible + : op_state(associate_data_t(ad).release(), r) { + } + + ~op_state() { + if (__assoc_) { + std::destroy_at(&__op_); + } + } + + void __run() noexcept { + if (__assoc_) { + stdexec::start(__op_); + } else { + stdexec::set_stopped(std::move(*__rcvr_)); + } + } + }; + + return op_state{__forward_like<_Self>(data), __rcvr}; + }; + + static constexpr auto start = [](auto& __state, auto&) noexcept -> void { + __state.__run(); + }; + }; + } // namespace __associate + + using __associate::associate_t; + + /// @brief The associate sender adaptor, which associates a sender with the + /// async scope referred to by the given token + /// @hideinitializer + inline constexpr associate_t associate{}; + + template <> + struct __sexpr_impl : __associate::__associate_impl { }; +} // namespace stdexec diff --git a/include/stdexec/__detail/__concepts.hpp b/include/stdexec/__detail/__concepts.hpp index 5a02d6636..718731050 100644 --- a/include/stdexec/__detail/__concepts.hpp +++ b/include/stdexec/__detail/__concepts.hpp @@ -253,6 +253,12 @@ namespace stdexec { template concept __nothrow_copy_constructible = (__nothrow_constructible_from<_Ts, const _Ts&> && ...); + template + concept __nothrow_assignable_from = STDEXEC_IS_NOTHROW_ASSIGNABLE(_Ty, _A); + + template + concept __nothrow_move_assignable = (__nothrow_assignable_from<_Ts, _Ts> && ...); + template concept __decay_copyable = (constructible_from<__decay_t<_Ts>, _Ts> && ...); diff --git a/include/stdexec/__detail/__config.hpp b/include/stdexec/__detail/__config.hpp index 645c2ac51..871e48c4d 100644 --- a/include/stdexec/__detail/__config.hpp +++ b/include/stdexec/__detail/__config.hpp @@ -377,6 +377,12 @@ namespace __coro = std::experimental; # define STDEXEC_IS_TRIVIALLY_CONSTRUCTIBLE(...) std::is_trivially_constructible_v<__VA_ARGS__> #endif +#if STDEXEC_HAS_BUILTIN(__is_nothrow_assignable) || STDEXEC_MSVC() +# define STDEXEC_IS_NOTHROW_ASSIGNABLE(...) __is_nothrow_assignable(__VA_ARGS__) +#else +# define STDEXEC_IS_NOTHROW_ASSIGNABLE(...) std::is_nothrow_assignable_v<__VA_ARGS__> +#endif + #if STDEXEC_HAS_BUILTIN(__is_empty) || STDEXEC_MSVC() # define STDEXEC_IS_EMPTY(...) __is_empty(__VA_ARGS__) #else diff --git a/include/stdexec/__detail/__scope_concepts.hpp b/include/stdexec/__detail/__scope_concepts.hpp new file mode 100644 index 000000000..456d734a0 --- /dev/null +++ b/include/stdexec/__detail/__scope_concepts.hpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 Ian Petersen + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "__execution_fwd.hpp" + +#include "__concepts.hpp" + +namespace stdexec { + ///////////////////////////////////////////////////////////////////////////// + // [exec.scope.concepts] + template + concept scope_association = movable<_Assoc> && __nothrow_move_constructible<_Assoc> + && __nothrow_move_assignable<_Assoc> && default_initializable<_Assoc> + && requires(const _Assoc assoc) { + { static_cast(assoc) } noexcept; + { assoc.try_associate() } -> same_as<_Assoc>; + }; + + namespace __scope_concepts { + struct __test_sender { + using sender_concept = stdexec::sender_t; + + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t(int), + stdexec::set_error_t(std::exception_ptr), + stdexec::set_stopped_t() + >; + + struct __op { + using operation_state_concept = stdexec::operation_state_t; + + __op() = default; + __op(__op&&) = delete; + + void start() & noexcept { + } + }; + + template + __op connect(_Receiver) { + return {}; + } + }; + } // namespace __scope_concepts + + template + concept scope_token = copyable<_Token> && requires(const _Token token) { + { token.try_associate() } -> scope_association; + { token.wrap(__declval<__scope_concepts::__test_sender>()) } -> sender_in>; + }; +} // namespace stdexec diff --git a/include/stdexec/__detail/__sender_adaptor_closure.hpp b/include/stdexec/__detail/__sender_adaptor_closure.hpp index 93f76aebf..b7f3bb658 100644 --- a/include/stdexec/__detail/__sender_adaptor_closure.hpp +++ b/include/stdexec/__detail/__sender_adaptor_closure.hpp @@ -69,7 +69,8 @@ namespace stdexec { template _Closure> STDEXEC_ATTRIBUTE(always_inline) - auto operator|(_Sender&& __sndr, _Closure&& __clsur) -> __call_result_t<_Closure, _Sender> { + auto operator|(_Sender&& __sndr, _Closure&& __clsur) + noexcept(__nothrow_callable<_Closure, _Sender>) -> __call_result_t<_Closure, _Sender> { return static_cast<_Closure&&>(__clsur)(static_cast<_Sender&&>(__sndr)); } diff --git a/include/stdexec/execution.hpp b/include/stdexec/execution.hpp index 6f282bbbd..b166aea77 100644 --- a/include/stdexec/execution.hpp +++ b/include/stdexec/execution.hpp @@ -19,6 +19,7 @@ // include these after __execution_fwd.hpp #include "__detail/__as_awaitable.hpp" // IWYU pragma: export +#include "__detail/__associate.hpp" // IWYU pragma: export #include "__detail/__basic_sender.hpp" // IWYU pragma: export #include "__detail/__bulk.hpp" // IWYU pragma: export #include "__detail/__completion_signatures.hpp" // IWYU pragma: export @@ -47,6 +48,7 @@ #include "__detail/__run_loop.hpp" // IWYU pragma: export #include "__detail/__schedule_from.hpp" // IWYU pragma: export #include "__detail/__schedulers.hpp" // IWYU pragma: export +#include "__detail/__scope_concepts.hpp" // IWYU pragma: export #include "__detail/__senders.hpp" // IWYU pragma: export #include "__detail/__sender_adaptor_closure.hpp" // IWYU pragma: export #include "__detail/__split.hpp" // IWYU pragma: export diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f403432e9..79ade771b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,6 +29,8 @@ set(stdexec_test_sources stdexec/concepts/test_concepts_receiver.cpp stdexec/concepts/test_concept_operation_state.cpp stdexec/concepts/test_concepts_sender.cpp + stdexec/concepts/test_concepts_scope_association.cpp + stdexec/concepts/test_concepts_scope_token.cpp stdexec/concepts/test_awaitables.cpp stdexec/algos/factories/test_just.cpp stdexec/algos/factories/test_transfer_just.cpp @@ -36,6 +38,7 @@ set(stdexec_test_sources stdexec/algos/factories/test_just_stopped.cpp stdexec/algos/factories/test_read.cpp stdexec/algos/factories/test_schedule.cpp + stdexec/algos/adaptors/test_associate.cpp stdexec/algos/adaptors/test_starts_on.cpp stdexec/algos/adaptors/test_on.cpp stdexec/algos/adaptors/test_on2.cpp diff --git a/test/stdexec/algos/adaptors/test_associate.cpp b/test/stdexec/algos/adaptors/test_associate.cpp new file mode 100644 index 000000000..a758ca16c --- /dev/null +++ b/test/stdexec/algos/adaptors/test_associate.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2025 Ian Petersen + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace ex = stdexec; + +namespace { + struct null_token { + struct assoc { + constexpr operator bool() const noexcept { + return true; + } + + constexpr assoc try_associate() const noexcept { + return {}; + } + }; + + template + constexpr Sender&& wrap(Sender&& sndr) const noexcept { + return std::forward(sndr); + } + + constexpr assoc try_associate() const noexcept { + return {}; + } + }; + + TEST_CASE("associate returns a sender", "[adaptors][associate]") { + auto snd = ex::associate(ex::just(), null_token{}); + STATIC_REQUIRE(ex::sender); + (void) snd; + } + + TEST_CASE("associate is appropriately noexcept", "[adaptors][associate]") { + // double-check our dependencies + STATIC_REQUIRE(noexcept(ex::just())); + STATIC_REQUIRE(noexcept(null_token{})); + + // null_token is no-throw default constructible and tokens must be no-throw + // copyable and movable so this whole thing had better be no-throw + STATIC_REQUIRE(noexcept(ex::associate(null_token{}))); + + // constructing and passing in a no-throw sender should let the whole + // expression be no-throw + STATIC_REQUIRE(noexcept(ex::associate(ex::just(), null_token{}))); + STATIC_REQUIRE(noexcept(ex::just() | ex::associate(null_token{}))); + + // conversely, trafficking in senders with potentially-throwing copy + // constructors should lead to the whole expression becoming potentially-throwing + const auto justString = ex::just(std::string{"Copying strings is potentially-throwing"}); + STATIC_REQUIRE(!noexcept(ex::associate(justString, null_token{}))); + STATIC_REQUIRE(!noexcept(justString | ex::associate(null_token{}))); + (void) justString; + } + + template + constexpr bool expected_completion_signatures() { + using expected_sigs = ex::completion_signatures; + using actual_sigs = ex::completion_signatures_of_t; + return expected_sigs{} == actual_sigs{}; + } + + TEST_CASE("associate has appropriate completion signatures", "[adaptors][associate]") { + STATIC_REQUIRE( + expected_completion_signatures< + decltype(ex::associate(ex::just(), null_token{})), + ex::set_value_t(), + ex::set_stopped_t() + >()); + + STATIC_REQUIRE( + expected_completion_signatures< + decltype(ex::associate(ex::just(std::string{}), null_token{})), + ex::set_value_t(std::string), + ex::set_stopped_t() + >); + + STATIC_REQUIRE( + expected_completion_signatures< + decltype(ex::associate(ex::just_stopped(), null_token{})), + ex::set_stopped_t() + >); + + STATIC_REQUIRE( + expected_completion_signatures< + decltype(ex::associate(ex::just_error(5), null_token{})), + ex::set_error_t(int), + ex::set_stopped_t() + >); + } + + // TODO: confirm that running an associate-sender produces the expected output + // variations: + // - with a null_token, it's the identity + // - with an always_expired_token, it's just_stopped + // - change the state of a token between copies; the copy is just_stopped + // TODO: confirm that `associate(foo(), token{})` destroys resources owned by foo() + // when token{} is expired + // TODO: check the pass-through nature of __sync_attrs + // TODO: check the pass-through stop request behaviour + // TODO: confirm that senders-of-references forward references when associated + // TODO: confirm timing of destruction of opstate relative to release of association + // TODO: confirm that the TODO list is exhaustive +} // namespace diff --git a/test/stdexec/concepts/test_concepts_scope_association.cpp b/test/stdexec/concepts/test_concepts_scope_association.cpp new file mode 100644 index 000000000..c77bcc6fa --- /dev/null +++ b/test/stdexec/concepts/test_concepts_scope_association.cpp @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2022 Ian Petersen + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace ex = stdexec; + +namespace { + + // a "null" association that is always truthy and for which try_associate() always succeeds + struct null_association { + // this need not be explicit, although it should be + constexpr operator bool() const noexcept { + return true; + } + + // this may throw, although it need not + constexpr null_association try_associate() const noexcept { + return {}; + } + }; + + // a CRTP base that lets us produce variations on null_associaton with the right return type on try_associate + template + struct crtp_association : null_association { + constexpr Derived try_associate() const noexcept { + return {}; + } + }; + + struct throwing_specials : crtp_association { + // it's ok for the non-move operators to throw + throwing_specials() noexcept(false) { + // nvcc doesn't respect the noexcept(false) with = default + } + throwing_specials(const throwing_specials&) noexcept(false) { + // nvcc doesn't respect the noexcept(false) with = default + } + throwing_specials(throwing_specials&&) noexcept = default; + ~throwing_specials() = default; + + throwing_specials& operator=(const throwing_specials&) noexcept(false) { + // nvcc doesn't respect the noexcept(false) with = default + return *this; + } + throwing_specials& operator=(throwing_specials&&) noexcept = default; + }; + + struct move_only : crtp_association { + // copy operations are not required + move_only() = default; + move_only(move_only&&) = default; + ~move_only() = default; + + move_only& operator=(move_only&&) = default; + }; + + struct explicit_bool : crtp_association { + // the bool conversion may be explicit + constexpr explicit operator bool() const noexcept { + return true; + } + }; + + struct throwing_reassociate : crtp_association { + // try_associate may throw + constexpr throwing_reassociate try_associate() const noexcept(false) { + return {}; + } + }; + + TEST_CASE( + "Scope association concept accepts basic association types", + "[concepts][scope_association]") { + // scope_association should accept the basic null_association + STATIC_REQUIRE(ex::scope_association); + + // the default constructor and copy operations may throw + STATIC_REQUIRE(ex::scope_association); + // double check that we're testing what we think we are + STATIC_REQUIRE(!ex::__nothrow_constructible_from); + STATIC_REQUIRE(!ex::__nothrow_constructible_from); + STATIC_REQUIRE(!ex::__nothrow_assignable_from); + + // copy operations are not required + STATIC_REQUIRE(ex::scope_association); + + // the bool conversion may be explicit + STATIC_REQUIRE(ex::scope_association); + + // try_associate may throw + STATIC_REQUIRE(ex::scope_association); + } + + // invalid association because of immovability + struct immovable : crtp_association { + immovable(immovable&&) = delete; + }; + + // conditionally-invalid association because of throwing move operations + template + struct throwing_moves : crtp_association> { + throwing_moves() = default; + throwing_moves(throwing_moves&&) noexcept(ThrowingCtor) { + // nvcc doesn't respect the noexcept(false) with = default + } + ~throwing_moves() = default; + + throwing_moves& operator=(throwing_moves&&) noexcept(ThrowingAssign) { + // nvcc doesn't respect the noexcept(false) with = default + return *this; + } + }; + + // invalid assocation because of a throwing move constructor + using throwing_move_ctor = throwing_moves; + // invalid assocation because of a throwing move assignment operator + using throwing_move_assign = throwing_moves; + + // invalid assocation because of a missing default constructor + struct missing_ctor : crtp_association { + missing_ctor() = delete; + }; + + // invalid assocation because of a throwing conversion to bool + struct throwing_boolish : crtp_association { + constexpr explicit operator bool() const noexcept(false) { + return true; + } + }; + + // invalid assocation because try_associate returns the wrong type + struct cannot_reassociate : null_association { }; + + // invalid association because operator bool is non-const + struct non_const_boolish : crtp_association { + constexpr explicit operator bool() noexcept { + return true; + } + }; + + // invalid association because try_associate is non-const + struct non_const_try_associate : null_association { + constexpr non_const_try_associate try_associate() noexcept { + return {}; + }; + }; + + TEST_CASE( + "Scope association concept rejects non-association types", + "[concepts][scope_association]") { + STATIC_REQUIRE(!ex::scope_association); + + // movability is required + STATIC_REQUIRE(!ex::scope_association); + + // the move operations must be non-throwing + STATIC_REQUIRE(!ex::scope_association); + STATIC_REQUIRE(!ex::scope_association); + + // default initialization is required + STATIC_REQUIRE(!ex::scope_association); + + // conversion to bool must not throw + STATIC_REQUIRE(!ex::scope_association); + + // try_associate must return an association + STATIC_REQUIRE(!ex::scope_association); + + // operator bool must be const qualified + STATIC_REQUIRE(!ex::scope_association); + + // try_associate must be const qualified + STATIC_REQUIRE(!ex::scope_association); + } +} // namespace diff --git a/test/stdexec/concepts/test_concepts_scope_token.cpp b/test/stdexec/concepts/test_concepts_scope_token.cpp new file mode 100644 index 000000000..78468b5d4 --- /dev/null +++ b/test/stdexec/concepts/test_concepts_scope_token.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2022 Ian Petersen + * Copyright (c) 2025 NVIDIA Corporation + * + * Licensed under the Apache License Version 2.0 with LLVM Exceptions + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace ex = stdexec; + +namespace { + + TEST_CASE("Scope token helpers are correctly defined", "[concepts][scope_token]") { + // check the test-sender and test-env definitions are appropriate + STATIC_REQUIRE(ex::sender); + STATIC_REQUIRE(ex::sender_in>); + STATIC_REQUIRE(ex::operation_state); + } + + // a "null" token that can always create new associations + struct null_token { + // the always-truthy association type + struct assoc { + // this need not be explicit, although it should be + constexpr operator bool() const noexcept { + return true; + } + + // this may throw, although it need not + constexpr assoc try_associate() const noexcept { + return {}; + } + }; + + constexpr assoc try_associate() const noexcept { + return {}; + } + + template + Sender&& wrap(Sender&& snd) const noexcept { + return std::forward(snd); + } + }; + + struct throwing_try_associate : null_token { + constexpr assoc try_associate() const noexcept(false) { + return {}; + } + }; + + struct throwing_wrap : null_token { + template + constexpr Sender&& wrap(Sender&& snd) const noexcept(false) { + return std::forward(snd); + } + }; + + struct wrapping_wrap : null_token { + template + struct wrapper : Sender { + wrapper(const Sender& snd) + : Sender(snd) { + } + + wrapper(Sender&& snd) + : Sender(std::move(snd)) { + } + }; + + template + auto wrap(Sender&& snd) const noexcept { + return wrapper{std::forward(snd)}; + } + }; + + TEST_CASE("Scope token concept accepts basic token types", "[concepts][scope_token]") { + // scope_token should accept the basic null_token + STATIC_REQUIRE(ex::scope_token); + + // it's ok for try_associate to throw + STATIC_REQUIRE(ex::scope_token); + + // it's ok for wrap to throw + STATIC_REQUIRE(ex::scope_token); + + // it's ok for wrap to change the type of its argument + STATIC_REQUIRE(ex::scope_token); + } + + struct move_only : null_token { + move_only() = default; + move_only(move_only&&) = default; + ~move_only() = default; + + move_only& operator=(move_only&&) = default; + }; + + struct non_const_try_associate : null_token { + assoc try_associate() noexcept { + return {}; + } + }; + + struct non_const_wrap : null_token { + template + Sender&& wrap(Sender&& snd) noexcept { + return std::forward(snd); + } + }; + + TEST_CASE("Scope token concept rejects non-token types", "[concepts][scope_token]") { + STATIC_REQUIRE(!ex::scope_token); + + // tokens must be copyable + STATIC_REQUIRE(!ex::scope_token); + + // try_associate must be const-qualified + STATIC_REQUIRE(!ex::scope_token); + + // wrap must be const-qualified + STATIC_REQUIRE(!ex::scope_token); + } +} // namespace