From d0096bca4470ed6d8822b361b1bd7b67493e9a50 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sun, 25 Jan 2026 09:55:28 -0500 Subject: [PATCH 1/2] stdexec::__closure: constexpr --- include/stdexec/__detail/__sender_adaptor_closure.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/stdexec/__detail/__sender_adaptor_closure.hpp b/include/stdexec/__detail/__sender_adaptor_closure.hpp index 102d1a56f..868cbcb5c 100644 --- a/include/stdexec/__detail/__sender_adaptor_closure.hpp +++ b/include/stdexec/__detail/__sender_adaptor_closure.hpp @@ -93,7 +93,8 @@ namespace STDEXEC { template requires __callable<_Fn, _Sender, _As...> STDEXEC_ATTRIBUTE(host, device, always_inline) - auto operator()(_Sender&& __sndr) && noexcept(__nothrow_callable<_Fn, _Sender, _As...>) { + constexpr auto + operator()(_Sender&& __sndr) && noexcept(__nothrow_callable<_Fn, _Sender, _As...>) { return STDEXEC::__apply( static_cast<_Fn&&>(__fn_), static_cast<__tuple<_As...>&&>(__args_), @@ -103,7 +104,7 @@ namespace STDEXEC { template requires __callable STDEXEC_ATTRIBUTE(host, device, always_inline) - auto operator()(_Sender&& __sndr) const & noexcept( + constexpr auto operator()(_Sender&& __sndr) const & noexcept( __nothrow_callable) { return STDEXEC::__apply(__fn_, __args_, static_cast<_Sender&&>(__sndr)); } From 8ee3a918bf42b3c1735c04a68c27625f208ba39a Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sun, 25 Jan 2026 11:29:33 -0500 Subject: [PATCH 2/2] stdexec::__apply: Do Not Deduce Return Type The type returned by stdexec::__apply was previously specified via decltype(auto). This is convenient to write, but means that in order to determine the return type the compiler must substitute into the actual body of the function. Doing this to constexpr functions causes Clang (at least up to 21.1.0) to fail to build the get_env function of many receiver types which are member types of an operation state (and which contain a reference back to that operation state). Consider this example which doesn't build on Clang 21.1.0 and which is intended to be similar to the aforementioned situation: template constexpr auto get(T& t) noexcept { return t.get(); } template struct state { using return_type = typename T::type; struct inner { constexpr return_type get() const noexcept; state& self; }; static_assert( std::is_same_v< decltype(get(std::declval())), int>); T t; }; template constexpr auto state::inner::get() const noexcept -> return_type { return self.t.get(); } struct t { using type = int; constexpr int get() const noexcept { return i; } int i; }; constexpr auto impl() noexcept { state s{t{5}}; state::inner i{s}; return get(i); } The error given by clang is that state is incomplete when it's used within state::inner::get. The backtrace associated with the compilation error identifies the static_assert as the problematic source of the use, if it is removed then Clang 21.1.0 accepts the above. This is eyebrow-raising for at least two reasons: - The point of use of state is within the out of line definition of state::inner::get which occurs lexically after state is complete - state::inner::get does not have a deduced return type, therefore the compiler has all the information necessary to determine the return type of get (the free function) without considering the definition of state::inner::get Codifying the second bullet by explicitly specifying the return type of get (the free function): constexpr auto get(T& t) noexcept -> decltype(t.get()) { return t.get(); } Causes Clang 21.1.0 to accept the above example (because it doesn't attempt to build the body of state::inner::get from a context whereat state is incomplete). Note that removing constexpr from state::inner::get causes Clang 21.1.0 to accept the original example. This discussion may seem to have nothing to do with stdexec::__apply until one examines the backtrace generated when Clang fails to build the get_env member function of affected receiver types: The backtrace radiates from building the body of stdexec::__apply. Explicitly specified the return type of stdexec::__apply to ameliorate the above. --- include/stdexec/__detail/__tuple.hpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/stdexec/__detail/__tuple.hpp b/include/stdexec/__detail/__tuple.hpp index 52ce15d21..f6818d3fd 100644 --- a/include/stdexec/__detail/__tuple.hpp +++ b/include/stdexec/__detail/__tuple.hpp @@ -219,9 +219,10 @@ namespace STDEXEC { template using __tuple_t = __mcall<_CvRef, __tuple<_Ts...>>; - template ...> _Fn> - void operator()(_Fn&& __fn, __tuple_t<_Ts...>&& __tupl, _Us&&... __us) const - noexcept(__nothrow_callable<_Fn, _Us..., __mcall<_CvRef, _Ts>...>); + template ...> _Fn> + auto operator()(_Fn&& __fn, __tuple_t<_Ts...>&& __tupl, _Us&&... __us) const + noexcept(__nothrow_callable<_Fn, _Us..., __mcall1<_CvRef, _Ts>...>) + -> __call_result_t<_Fn, _Us..., __mcall1<_CvRef, _Ts>...>; }; template @@ -232,7 +233,8 @@ namespace STDEXEC { requires __callable<__impl_t<_Tuple>, _Fn, _Tuple, _Us...> STDEXEC_ATTRIBUTE(always_inline, host, device) constexpr auto operator()(_Fn&& __fn, _Tuple&& __tupl, _Us&&... __us) const - noexcept(__nothrow_callable<__impl_t<_Tuple>, _Fn, _Tuple, _Us...>) -> decltype(auto) { + noexcept(__nothrow_callable<__impl_t<_Tuple>, _Fn, _Tuple, _Us...>) + -> __call_result_t<__impl_t<_Tuple>, _Fn, _Tuple, _Us...> { constexpr size_t __size = STDEXEC_REMOVE_REFERENCE(_Tuple)::__size; if constexpr (__size == 0) {