diff --git a/include/exec/any_sender_of.hpp b/include/exec/any_sender_of.hpp index 93dbe7805..3e811320a 100644 --- a/include/exec/any_sender_of.hpp +++ b/include/exec/any_sender_of.hpp @@ -16,6 +16,7 @@ #pragma once #include "../stdexec/__detail/__any.hpp" +#include "../stdexec/__detail/__concepts.hpp" #include "../stdexec/__detail/__receiver_ref.hpp" #include "../stdexec/__detail/__receivers.hpp" @@ -28,8 +29,35 @@ STDEXEC_PRAGMA_IGNORE_GNU("-Woverloaded-virtual") namespace experimental::execution { + namespace _qry_detail + { + template + struct _env_archetype; + + template + struct _env_archetype + { + Return query(Query, Args &&...) const noexcept(Nothrow); + }; + + using namespace STDEXEC; + + template + inline constexpr bool is_query_function_v = false; + + template + inline constexpr bool is_query_function_v = + __callable const &, Args...>; + + template + inline constexpr bool is_query_function_v = + __nothrow_callable const &, Args...>; + } // namespace _qry_detail + template - struct queries; + requires(_qry_detail::is_query_function_v && ...) + struct queries + {}; template > struct any_receiver; diff --git a/include/exec/function.hpp b/include/exec/function.hpp new file mode 100644 index 000000000..b41b43684 --- /dev/null +++ b/include/exec/function.hpp @@ -0,0 +1,529 @@ +/* Copyright (c) 2026 Ian Petersen + * Copyright (c) 2026 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 "../stdexec/__detail/__completion_signatures.hpp" +#include "../stdexec/__detail/__concepts.hpp" +#include "../stdexec/__detail/__meta.hpp" +#include "../stdexec/__detail/__read_env.hpp" +#include "../stdexec/__detail/__receivers.hpp" +#include "../stdexec/__detail/__sender_concepts.hpp" +#include "../stdexec/__detail/__static_vector.hpp" +#include "../stdexec/__detail/__tuple.hpp" +#include "../stdexec/__detail/__typeinfo.hpp" +#include "../stdexec/__detail/__utility.hpp" +#include "../stdexec/functional.hpp" + +// TODO: split this header into pieces +#include "any_sender_of.hpp" + +#include +#include +#include + +// This file defines function, which is a +// type-erased sender that can complete with +// - set_value(ReturnType) +// - set_error(std::exception_ptr) +// - set_stopped() +// +// The type-erased operation state is allocated in connect; to accomplish +// this deferred allocation, the sender holds a tuple of arguments that +// are passed into a sender-factory in connect, which is why the template +// type parameter is a function type rather than just a return type. +// +// The intended use case is an ABI-stable API boundary, assuming that a +// std::tuple qualifies as "ABI-stable". The hope is that +// this is a "better task" in that it represents an async function from +// arguments to value, just like a task coroutine, but, by deferring the +// allocation to connect, we can use receiver environment queries to pick +// the frame allocator from the environment without relying on TLS. +namespace experimental::execution +{ + // A forwarding query for a "frame allocator", to be used for dynamically allocating + // the operation states of senders type-erased by exec::function. + struct get_frame_allocator_t : STDEXEC::__query + { + using STDEXEC::__query::operator(); + + constexpr auto operator()() const noexcept + { + return STDEXEC::read_env(get_frame_allocator_t{}); + } + + template + static constexpr void __validate() noexcept + { + static_assert(STDEXEC::__nothrow_callable); + using __alloc_t = STDEXEC::__call_result_t; + static_assert(STDEXEC::__simple_allocator>); + } + + static consteval auto query(STDEXEC::forwarding_query_t) noexcept -> bool + { + return true; + } + }; + + inline constexpr get_frame_allocator_t get_frame_allocator{}; + + namespace _func + { + using namespace STDEXEC; + + template + class _func_op; + + // The concrete operation state resulting from connecting a function<...> to a concrete + // receiver of type Receiver. This type manages a dynamically-allocated _derived_op instance, + // which is the type-erased operation state resulting from connecting the type-erased sender + // to an _any::_any_receiver_ref with the given completion signatures and queries. + template + class _func_op, Queries...> + { + using _receiver_t = + ::exec::_any::_any_receiver_ref, queries>; + + using _stop_token_t = stop_token_of_t>; + + // rcvr_ has to be initialized before op_ because our implementation of get_env + // is empirically accessed during our constructor and depends on rcvr_ being initialized + _any::_state rcvr_; + _any::_any_opstate_base op_; + + public: + using operation_state_concept = operation_state_tag; + + template + explicit constexpr _func_op(Receiver rcvr, Factory factory) + : rcvr_(static_cast(rcvr)) + , op_(factory(_receiver_t(rcvr_))) + {} + + _func_op(_func_op &&) = delete; + + constexpr ~_func_op() = default; + + constexpr void start() & noexcept + { + op_.start(); + } + }; + + // given the concrete receiver's environment, choose the frame allocator; first choice + // is the result of get_frame_allocator(env), second choice is get_allocator(env), and + // the default is std::allocator + inline constexpr auto choose_frame_allocator = + STDEXEC::__first_callable{get_frame_allocator, + get_allocator, + STDEXEC::__always{std::allocator()}}; + + template + class _func_impl; + + // the main implementation of the type-erasing sender function<...> + // + // SndrCncpt should be std::execution::sender_concept + // Args... is the argument types used to construct the erased sender + // Sigs... is the supported completion signatures + // Queries... is the list of environment queries that must be supported by the eventual + // receiver; it's a pack of function type like Return(Query, Args...) or + // Return(Query, Args...) noexcept. The named query, when given the specified + // arguments, must return a value convertible to Return, and it must be noexcept, + // or not, as appropriate + template + class _func_impl, queries> + { + using _receiver_t = + ::exec::_any::_any_receiver_ref, queries>; + + template + using _func_op_t = _func_op, Queries...>; + + // The type-erased operation state factory; it points to a function that knows the concrete + // type of the sender factory stored in make_sender_ so that it can construct the desired + // sender on demand and connect it to the given receiver. The expected arguments are the + // address of make_sender_, the _any_receiver_ref to connect the sender to, and the arguments + // to pass to make_sender_ to construct the sender. + _any::_any_opstate_base (*make_op_)(void *, _receiver_t, Args &&...); + // Storage for the sender factory passed to our constructor template; make_op_ will + // reconstitute the actual factory from this bag-of-bytes with start_lifetime_as + // because it internally knows the concrete type of the user-provided sender factory. + // We're reserving 2 * sizeof(void *) bytes to permit the factory to be a pointer to + // member function, which usually requires two pointers. + std::byte make_sender_[2 * sizeof(void *)]{}; + // The curried arguments that will be passed to make_sender_ from inside make_op_. + STDEXEC_ATTRIBUTE(no_unique_address) + STDEXEC::__tuple args_; + + public: + using sender_concept = SndrCncpt; + + template Factory> + requires STDEXEC::__not_decays_to // + && (STDEXEC_IS_TRIVIALLY_COPYABLE(Factory)) // + && (sizeof(Factory) <= sizeof(make_sender_)) // + && STDEXEC::sender_to, _receiver_t> + constexpr explicit _func_impl(Args &&...args, Factory factory) + noexcept(STDEXEC::__nothrow_move_constructible) + : args_(static_cast(args)...) + { + using sender_t = __invoke_result_t; + + std::memcpy(make_sender_, std::addressof(factory), sizeof(Factory)); + + make_op_ = [](void *storage, _receiver_t rcvr, Args &&...args) -> _any::_any_opstate_base + { + auto &make_sender = *__std::start_lifetime_as(storage); + + auto alloc = choose_frame_allocator(get_env(rcvr)); + + return _any::_any_opstate_base(__in_place_from, + std::allocator_arg, + alloc, + STDEXEC::connect, + STDEXEC::__invoke(make_sender, + static_cast(args)...), + static_cast<_receiver_t &&>(rcvr)); + }; + } + + template <__std::derived_from<_func_impl> Func> + requires __not_decays_to + constexpr _func_impl(Func &&other) noexcept(__nothrow_move_constructible<__tuple>) + : _func_impl(static_cast<_func_impl &&>(other)) + {} + + template <__std::derived_from<_func_impl> Func> + requires __not_decays_to && __std::copy_constructible<__tuple> + constexpr _func_impl(Func const &other) + noexcept(__nothrow_copy_constructible<__tuple>) + : _func_impl(static_cast<_func_impl const &>(other)) + {} + + // this implementation of get_completion_signatures is taken directly from + // the equivalent function on any_sender_of + template + static consteval auto get_completion_signatures() + { + static_assert(__std::derived_from, _func_impl>); + + // throw if Env does not contain the queries needed to type-erase the receiver: + using _check_queries_t = __mfind_error<_any::_check_query_t...>; + if constexpr (__merror<_check_queries_t>) + { + return STDEXEC::__throw_compile_time_error(_check_queries_t{}); + } + else + { + return completion_signatures{}; + } + } + + template + constexpr _func_op_t connect(Receiver rcvr) && + { + return _func_op_t{static_cast(rcvr), + [this](RcvrRef rcvr) + { + return STDEXEC::__apply(make_op_, + static_cast<__tuple &&>( + args_), + make_sender_, + static_cast(rcvr)); + }}; + } + + template + requires STDEXEC::__std::copy_constructible<_func_impl> + constexpr _func_op_t connect(Receiver rcvr) const & + { + return _func_impl(*this).connect(static_cast(rcvr)); + } + }; + + template + struct _canonical_fn; + + template + struct _canonical_fn> + { + consteval auto operator()() const noexcept + { + constexpr auto make_sigs = []() noexcept + { + return __cmplsigs::__to_array(completion_signatures{}); + }; + + return __cmplsigs::__completion_sigs_from(make_sigs); + } + }; + + template + struct _canonical_fn> + { + private: + // sort and unique the function types in Queries... into an array of __mtypeids + static consteval auto get_sigs() noexcept + { + using sig_array_t = __static_vector<__type_index, sizeof...(Queries)>; + auto sigs = sig_array_t{__mtypeid...}; + + std::ranges::sort(sigs); + + auto const end = std::ranges::unique(sigs).begin(); + sigs.erase(end, sigs.end()); + + return sigs; + } + + public: + consteval auto operator()() const noexcept + { + constexpr auto sigs = get_sigs(); + + constexpr auto fn = [=](__indices) + { + return queries<__msplice...>(); + }; + + return fn(__make_indices()); + } + }; + + template + inline constexpr _canonical_fn _canonical{}; + + template + using _canonical_t = decltype(_canonical()); + + // Given a return type and a bool indicating whether the function is noexcept, + // compute the appropriate completion_signatures. The result is a set_value + // overload taking either Return&& or no args when Return is void, set_stopped, + // and, when the function type is not noexcept, set_error(std::exception_ptr) + template + using _sigs_from_t = _canonical_t, + STDEXEC::set_stopped_t()>, + STDEXEC::__eptr_completion_unless_t>>>; + + template + struct _func_ops_crtp + { + template + requires std::same_as + Derived &operator=(Base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + Base::operator=(static_cast(other)); + return *static_cast(this); + } + + template + requires std::same_as && STDEXEC::__copy_assignable + Derived &operator=(Base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + { + Base::operator=(other); + return *static_cast(this); + } + }; + } // namespace _func + + // the user-facing interface to exec::function that supports several different declaration + // styles, including: + // - function: a fallible function from (bar, baz) to int + // - function: an infallible function from (bar, baz) to int + // - function>: a function from (bar, baz) + // that completes in the ways specified by the given specialization of completion_signatures + // - function: a function from (bar, baz) + // to int that requires the final receiver to have an environment that supports the + // Query query, taking arguments Args..., and returning an object convertible to Return; queries + // may be required to be no-throw by delcaring the function type noexcept + // - function< + // sender_tag(bar, baz), + // completion_signatures<...>, + // queries>: a fully-specified async function that maps (bar, baz) + // to the specified completions, requiring the specified queries in the ultimate receiver's + // environment + // + // Future: support C-style ellipsis arguments in the function signature to permit type-erased + // arguments as well, like function (a fallible function from + // (bar, baz) plus unspecified, erased additional arguments to int) + template + struct function; + + template + // should this require STDEXEC::__not_same_as? + // + // you *could* write STDEXEC::just(STDEXEC::sender_tag{}), but it seems more likely + // that invoking this specialization with Return set to sender_tag is a bug... + // + // the same question applies to all the specializations below that take explicit + // completion signatures + struct function + : _func::_func_impl, queries<>> + , _func::_func_ops_crtp> + { + private: + using base = _func::_func_impl, + queries<>>; + + template class, template class> + friend struct std::basic_common_reference; + + public: + using base::base; + }; + + template + struct function + : _func::_func_impl, queries<>> + , _func::_func_ops_crtp> + { + private: + using base = + _func::_func_impl, queries<>>; + + template class, template class> + friend struct std::basic_common_reference; + + public: + using base::base; + }; + + template + requires STDEXEC::__is_instance_of + struct function + : _func::_func_impl, queries<>> + , _func::_func_ops_crtp> + { + private: + using base = + _func::_func_impl, queries<>>; + + template class, template class> + friend struct std::basic_common_reference; + + public: + using base::base; + }; + + template + struct function> + : _func::_func_impl, + _func::_canonical_t>> + , _func::_func_ops_crtp>> + { + private: + using base = _func::_func_impl, + _func::_canonical_t>>; + + template class, template class> + friend struct std::basic_common_reference; + + public: + using base::base; + }; + + template + struct function> + : _func::_func_impl, + _func::_canonical_t>> + , _func::_func_ops_crtp>> + { + private: + using base = _func::_func_impl, + _func::_canonical_t>>; + + template class, template class> + friend struct std::basic_common_reference; + + public: + using base::base; + }; + + template + struct function, + queries> + : _func::_func_impl>, + _func::_canonical_t>> + , _func::_func_ops_crtp, + queries>> + { + private: + using base = _func::_func_impl>, + _func::_canonical_t>>; + + template class, template class> + friend struct std::basic_common_reference; + + public: + using base::base; + }; +} // namespace experimental::execution + +namespace exec = experimental::execution; + +namespace std +{ + template class TQual, template class UQual> + struct basic_common_reference, + typename exec::function::base, + TQual, + UQual> + { + private: + using base = exec::function::base; + + public: + using type = common_reference_t, UQual>; + }; + + template class TQual, template class UQual> + struct basic_common_reference::base, + exec::function, + TQual, + UQual> + { + private: + using base = exec::function::base; + + public: + using type = common_reference_t, UQual>; + }; + + template class TQual, + template class UQual> + struct basic_common_reference, exec::function, TQual, UQual> + { + private: + using tbase = exec::function::base; + using ubase = exec::function::base; + + public: + using type = common_reference_t, UQual>; + }; +} // namespace std diff --git a/include/stdexec/__detail/__any.hpp b/include/stdexec/__detail/__any.hpp index 3a1a051ea..af73c0d2d 100644 --- a/include/stdexec/__detail/__any.hpp +++ b/include/stdexec/__detail/__any.hpp @@ -465,6 +465,7 @@ namespace STDEXEC::__any : __val_(static_cast<_Args &&>(__args)...) {} +#if !STDEXEC_GCC() template constexpr explicit __box(__in_place_from_t, _Fn &&__fn, _Args &&...__args) noexcept(__nothrow_callable<_Fn, _Args...>) @@ -472,6 +473,50 @@ namespace STDEXEC::__any { static_assert(__same_as<__call_result_t<_Fn, _Args...>, _Value>); } +#else + template + constexpr explicit __box(__in_place_from_t, _Fn &&__fn, _Args &&...__args) + noexcept(__nothrow_callable<_Fn, _Args...>) + { + // for unknown reasons, GCC (sometimes) doesn't like initializing __val_ with the + // result of this function call in the member initialization clause when _Value + // is immovable even though the conditions should be right to trigger C++17's + // mandatory copy elision, but this in-place new into an uninitialized anonymous + // union member works just fine + static_assert(__same_as<__call_result_t<_Fn, _Args...>, _Value>); + new ((void *) std::addressof(__val_)) + _Value(static_cast<_Fn &&>(__fn)(static_cast<_Args &&>(__args)...)); + } + + constexpr __box(__box &&__other) noexcept(__nothrow_move_constructible<_Value>) + requires __std::move_constructible<_Value> + : __val_(static_cast<_Value &&>(__other).__val_) + {} + + constexpr __box(__box const &__other) noexcept(__nothrow_copy_constructible<_Value>) + requires __std::copy_constructible<_Value> + : __val_(__other.__val_) + {} + + constexpr ~__box() + { + __val_.~_Value(); + } + + constexpr __box &operator=(__box &&__rhs) noexcept(__nothrow_move_assignable<_Value>) + requires __move_assignable<_Value> + { + __val_ = static_cast<__box &&>(__rhs).__val_; + return *this; + } + + constexpr __box &operator=(__box const &__rhs) noexcept(__nothrow_copy_assignable<_Value>) + requires __copy_assignable<_Value> + { + __val_ = __rhs.__val_; + return *this; + } +#endif template [[nodiscard]] @@ -481,8 +526,16 @@ namespace STDEXEC::__any } private: +#if !STDEXEC_GCC() STDEXEC_ATTRIBUTE(no_unique_address) _Value __val_; +#else + union + { + STDEXEC_ATTRIBUTE(no_unique_address) + _Value __val_; + }; +#endif }; template diff --git a/include/stdexec/__detail/__concepts.hpp b/include/stdexec/__detail/__concepts.hpp index af43ca566..094a2505c 100644 --- a/include/stdexec/__detail/__concepts.hpp +++ b/include/stdexec/__detail/__concepts.hpp @@ -300,12 +300,24 @@ namespace STDEXEC template concept __nothrow_copy_constructible = (__nothrow_constructible_from<_Ts, _Ts const &> && ...); + template + concept __assignable_from = STDEXEC_IS_ASSIGNABLE(_Ty, _A); + template concept __nothrow_assignable_from = STDEXEC_IS_NOTHROW_ASSIGNABLE(_Ty, _A); + template + concept __move_assignable = (__assignable_from<_Ts, _Ts> && ...); + template concept __nothrow_move_assignable = (__nothrow_assignable_from<_Ts, _Ts> && ...); + template + concept __copy_assignable = (__assignable_from<_Ts, _Ts const &> && ...); + + template + concept __nothrow_copy_assignable = (__nothrow_assignable_from<_Ts, _Ts const &> && ...); + template concept __decay_copyable = (__std::constructible_from<__decay_t<_Ts>, _Ts> && ...); diff --git a/include/stdexec/__detail/__config.hpp b/include/stdexec/__detail/__config.hpp index 908d12655..2e20a26ba 100644 --- a/include/stdexec/__detail/__config.hpp +++ b/include/stdexec/__detail/__config.hpp @@ -520,6 +520,12 @@ namespace STDEXEC::__std # define STDEXEC_IS_NOTHROW_ASSIGNABLE(...) std::is_nothrow_assignable_v<__VA_ARGS__> #endif +#if STDEXEC_HAS_BUILTIN(__is_assignable) || STDEXEC_MSVC() +# define STDEXEC_IS_ASSIGNABLE(...) __is_assignable(__VA_ARGS__) +#else +# define STDEXEC_IS_ASSIGNABLE(...) std::is_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/__static_vector.hpp b/include/stdexec/__detail/__static_vector.hpp index 0f01046d8..ae81bc198 100644 --- a/include/stdexec/__detail/__static_vector.hpp +++ b/include/stdexec/__detail/__static_vector.hpp @@ -159,6 +159,18 @@ namespace STDEXEC __static_vector() = default; + [[nodiscard]] + constexpr auto operator[](std::size_t) noexcept -> value_type & + { + STDEXEC_ASSERT(false); + } + + [[nodiscard]] + constexpr auto operator[](std::size_t) const noexcept -> value_type const & + { + STDEXEC_ASSERT(false); + } + [[nodiscard]] constexpr auto begin() noexcept -> iterator { @@ -194,6 +206,14 @@ namespace STDEXEC { return 0; } + + constexpr auto erase(const_iterator __first, const_iterator __last) noexcept -> iterator + { + STDEXEC_ASSERT(__first == __last); + STDEXEC_ASSERT(__first == nullptr); + + return end(); + } }; template ... _Rest> diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 93a30070a..388143fad 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -61,6 +61,7 @@ set(exec_test_sources $<$>:sequence/test_merge_each_threaded.cpp> $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp + test_function.cpp ) set_source_files_properties(test_any_sender.cpp diff --git a/test/exec/test_function.cpp b/test/exec/test_function.cpp new file mode 100644 index 000000000..58b490845 --- /dev/null +++ b/test/exec/test_function.cpp @@ -0,0 +1,418 @@ +/* + * Copyright (c) 2026 Ian Petersen + * Copyright (c) 2026 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 + +#include + +#include + +namespace ex = STDEXEC; + +namespace +{ + TEST_CASE("exec::function is constructible", "[types][function]") + { + SECTION("void()") + { + exec::function sndr([]() noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("int()") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("void(int, double&)") + { + double d = 4.; + exec::function sndr(5, + d, + [](int, double &) noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("void() noexcept") + { + exec::function sndr([]() noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("int() noexcept") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("sender_tag() with only set_value_t(int)") + { + exec::function> sndr( + []() noexcept { return ex::just(42); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("sender_tag() with only set_stopped_t()") + { + exec::function> sndr( + []() noexcept { return ex::just_stopped(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("void() with trivial custom environment") + { + exec::function> sndr([]() noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + + SECTION("sender_tag(int) with only set_value_t() and trivial environment") + { + exec::function, + exec::queries<>> + sndr(5, [](int) noexcept { return ex::just(); }); + STATIC_REQUIRE(STDEXEC::sender); + } + } + + TEST_CASE("exec::function is connectable", "[types][function]") + { + SECTION("int() noexcept from just(42)") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + + auto [fortytwo] = ex::sync_wait(std::move(sndr)).value(); + + REQUIRE(fortytwo == 42); + } + + SECTION("void() from throwing factory") + { + exec::function sndr([]() -> decltype(ex::just()) { throw "oops"; }); + + REQUIRE_THROWS(ex::sync_wait(std::move(sndr))); + } + + SECTION("void() from throwing then") + { + exec::function sndr([]() noexcept + { return ex::just() | ex::then([] { throw "oops"; }); }); + + REQUIRE_THROWS(ex::sync_wait(std::move(sndr))); + } + + SECTION("void() from just_stopped()") + { + exec::function sndr([]() noexcept { return ex::just_stopped(); }); + + auto ret = ex::sync_wait(std::move(sndr)); + + REQUIRE_FALSE(ret.has_value()); + } + + SECTION("custom completions from just_error(42)") + { + exec::function> + sndr([]() noexcept { return ex::just_error(42); }); + + REQUIRE_THROWS_AS(ex::sync_wait(std::move(sndr)), int); + } + } + + TEST_CASE("exec::function forwards get_frame_allocator", "[types][function]") + { + // TODO: you probably shouldn't have to specify the frame allocator query like this + using Queries = exec::queries( + exec::get_frame_allocator_t) noexcept>; + + exec::function sndr( + []() noexcept + { + return ex::read_env(exec::get_frame_allocator) + | ex::then( + [](auto alloc) noexcept + { + return std::same_as, decltype(alloc)>; + }); + }); + + std::pmr::polymorphic_allocator alloc; + + auto [ret] = ex::sync_wait(std::move(sndr) + | ex::write_env(ex::prop(exec::get_frame_allocator, alloc))) + .value(); + + REQUIRE(ret); + } + + TEST_CASE("exec::function is conditionally lvalue connectable", "[types][function]") + { + exec::function sndr([]() noexcept { return ex::just(42); }); + + auto [ret] = ex::sync_wait(sndr).value(); + + REQUIRE(ret == 42); + } + + TEST_CASE("exec::function accepts lvalue callables", "[types][function]") + { + exec::function sndr(42, ex::just); + + auto [ret] = ex::sync_wait(sndr).value(); + + REQUIRE(ret == 42); + } + + struct iface + { + virtual exec::function get_i_virtually() const noexcept = 0; + }; + + struct iface2 + { + exec::function get_i_from_base() const noexcept + { + return exec::function(this, &iface2::get_i_virtually); + } + + virtual exec::function get_i_virtually() const noexcept = 0; + }; + + struct impl + : iface + , iface2 + { + explicit impl(int i) noexcept + : i_(i) + {} + + auto just_i() const noexcept + { + return ex::just(i_); + } + + static auto static_just_i(impl const *self) noexcept + { + return self->just_i(); + } + + exec::function get_i_with_capture() const noexcept + { + return exec::function([this]() noexcept { return just_i(); }); + } + + exec::function get_i_with_pmfn() const noexcept + { + return exec::function(this, &impl::just_i); + } + + exec::function get_i_virtually() const noexcept override + { + return get_i_with_capture(); + } + + private: + int i_; + }; + + TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + { + SECTION("function accepts a lambda capturing this") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_with_capture()).value(); + + REQUIRE(ret == 42); + } + + SECTION("function accepts a pointer-to-member function") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_with_pmfn()).value(); + + REQUIRE(ret == 42); + } + + SECTION("function accepts a pointer-to-function") + { + impl imp{42}; + auto [ret] = ex::sync_wait( + exec::function(&imp, &impl::static_just_i)) + .value(); + + REQUIRE(ret == 42); + } + + SECTION("function can be the return type of a virtual member function") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_virtually()).value(); + + REQUIRE(ret == 42); + } + + SECTION("function accepts a pointer-to-member function") + { + impl imp{42}; + auto [ret] = + ex::sync_wait(exec::function(&imp, &iface::get_i_virtually)).value(); + + REQUIRE(ret == 42); + } + + SECTION("function works on the base class") + { + auto [ret] = ex::sync_wait(impl{42}.get_i_from_base()).value(); + + REQUIRE(ret == 42); + } + } + + TEST_CASE("completion_signature specification is order-independent", "[types][function]") + { + // by specifying the completions with a function signature, it's up to the library what + // order the completion signatures are specified in + using func1_t = exec::function; + // this declaration chooses value before stopped + using func2_t = + exec::function>; + // this declaration chooses stopped before value + using func3_t = + exec::function>; + + SECTION("the function types are not the same as each other") + { + STATIC_REQUIRE(!std::same_as); + STATIC_REQUIRE(!std::same_as); + STATIC_REQUIRE(!std::same_as); + } + + SECTION("move-construction works in every direction between all three types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("copy-construction works in every direction between all three types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("move-assignment works in every direction between all three types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + + SECTION("copy-assignment works in every direction between all three types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + } + + TEST_CASE("queries specification is order-independent", "[types][function]") + { + constexpr auto query1 = [](auto const &) noexcept + { + return 0; + }; + + constexpr auto query2 = [](auto const &, int i) + { + return (double) i; + }; + + using query1_t = decltype(query1); + using query2_t = decltype(query2); + + using func1_t = + exec::function>; + + using func2_t = + exec::function>; + + SECTION("the function types are not the same as each other") + { + STATIC_REQUIRE(!std::same_as); + } + + SECTION("move construction works in all directions with both types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("copy construction works in all directions with both types") + { + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + STATIC_REQUIRE(std::constructible_from); + } + + SECTION("move-assignment works in every direction with both types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + + SECTION("copy-assignment works in every direction with both types") + { + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + STATIC_REQUIRE(std::assignable_from); + } + } +} // namespace