diff --git a/include/exec/any_sender_of.hpp b/include/exec/any_sender_of.hpp index 93dbe7805..8e538d7b6 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,7 +29,33 @@ 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 + requires(_qry_detail::is_query_function_v && ...) struct queries; template > diff --git a/include/exec/function.hpp b/include/exec/function.hpp new file mode 100644 index 000000000..07629cfa4 --- /dev/null +++ b/include/exec/function.hpp @@ -0,0 +1,584 @@ +/* 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/__read_env.hpp" +#include "../stdexec/__detail/__receivers.hpp" +#include "../stdexec/__detail/__sender_concepts.hpp" +#include "../stdexec/__detail/__tuple.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 + template + constexpr auto choose_frame_allocator(Env const &env) noexcept + { + if constexpr (requires { get_frame_allocator(env); }) + { + return get_frame_allocator(env); + } + else if constexpr (requires { get_allocator(env); }) + { + return get_allocator(env); + } + else + { + return std::allocator(); + } + } + + template + bool _equal(std::index_sequence, __tuple const &lhs, __tuple const &rhs) + noexcept(noexcept(((__get(lhs) == __get(rhs)) && ...))) + { + return ((__get(lhs) == __get(rhs)) && ...); + } + + template + bool _equal(__tuple const &lhs, __tuple const &rhs) + noexcept(noexcept(_equal(std::index_sequence_for{}, lhs, rhs))) + { + return _equal(std::index_sequence_for{}, lhs, rhs); + } + + 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_; + + // equal args and equal pointers-to-factories are equal + friend constexpr bool operator==(_func_impl const &lhs, _func_impl const &rhs) + noexcept(noexcept(_equal(lhs.args_, rhs.args_))) + { + return lhs.make_op_ == rhs.make_op_ + && std::ranges::equal(lhs.make_sender_, rhs.make_sender_) + && _equal(lhs.args_, rhs.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, + std::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_t; + + template + struct _canonical_t> + { + consteval auto operator()() const noexcept + { + constexpr auto make_sigs = []() noexcept + { + return __cmplsigs::__to_array(completion_signatures{}); + }; + + return __cmplsigs::__completion_sigs_from(make_sigs); + } + }; + + template + inline constexpr _canonical_t _canonical{}; + + template + using _canonical_sigs_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_sigs_t, + STDEXEC::set_stopped_t()>, + STDEXEC::__eptr_completion_unless_t>>>; + } // 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<>> + { + using base = _func::_func_impl, + queries<>>; + + using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } + }; + + template + struct function + : _func::_func_impl, queries<>> + { + using base = + _func::_func_impl, queries<>>; + + using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } + }; + + template + requires STDEXEC::__is_instance_of + struct function + : _func::_func_impl, queries<>> + { + using base = + _func::_func_impl, queries<>>; + + using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } + }; + + template + struct function> + : _func::_func_impl, + queries> + { + using base = _func::_func_impl, + queries>; + + using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } + }; + + template + struct function> + : _func::_func_impl, + queries> + { + using base = _func::_func_impl, + queries>; + + using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } + }; + + template + struct function, + queries> + : _func::_func_impl>, + queries> + { + using base = + _func::_func_impl>, + queries>; + + using base::base; + + function(function &&) = default; + function(function const &) = default; + + ~function() = default; + + function &operator=(function &&) = default; + function &operator=(function const &) = default; + + function &operator=(base &&other) noexcept(STDEXEC::__nothrow_move_assignable) + { + base::operator=(static_cast(other)); + return *this; + }; + + function &operator=(base const &other) noexcept(STDEXEC::__nothrow_copy_assignable) + requires STDEXEC::__copy_assignable + { + base::operator=(other); + return *this; + } + }; +} // 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, 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/__concepts.hpp b/include/stdexec/__detail/__concepts.hpp index 7e2bcb6b0..c30fba216 100644 --- a/include/stdexec/__detail/__concepts.hpp +++ b/include/stdexec/__detail/__concepts.hpp @@ -298,12 +298,21 @@ 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 __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 3106b08fa..9b2e8d816 100644 --- a/include/stdexec/__detail/__config.hpp +++ b/include/stdexec/__detail/__config.hpp @@ -501,6 +501,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/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..b3e88bfc0 --- /dev/null +++ b/test/exec/test_function.cpp @@ -0,0 +1,394 @@ +/* + * 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); + } + + TEST_CASE("exec::function accepts small trivially-copyable callables", "[types][function]") + { + 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_; + }; + + 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("...but they all inherit from the same _func_impl base") + { + 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); + } + + SECTION("instances are mutually comparable with ==") + { + // identical copies in slightly different types + func1_t f1(42, ex::just); + func2_t f2(f1); + func3_t f3(f1); + + // differing curried arguments from above + func2_t f4(45, ex::just); + func3_t f5(45, ex::just); + + REQUIRE(f1 == f1); + REQUIRE(f1 == f2); + REQUIRE(f1 == f3); + REQUIRE(f2 == f1); + REQUIRE(f2 == f2); + REQUIRE(f2 == f3); + REQUIRE(f3 == f1); + REQUIRE(f3 == f2); + REQUIRE(f3 == f3); + + REQUIRE(f1 != f4); + REQUIRE(f2 != f4); + REQUIRE(f3 != f4); + + REQUIRE(f4 == f5); + } + } +} // namespace