AUTHOR NOTE: our recent attempted upgrade to using the latest stdexec failed badly due to the below issue, as reported by Claude. There is a workaround of nested senders, eg. sequence(a, sequence(b, c)), which ensures each sequence is limited to two senders, but that feels a bit rubbish so we are reverting to the old version for now.
Summary
A type-erased exec::any_sender composes fine in a 2-element exec::sequence, but putting it in a sequence of three or more senders fails to compile with no matching function for call to 'get_completion_signatures'. A 3-element sequence of concrete senders is fine.
Reproducer
// clang++ -std=c++23 -I<stdexec>/include -DCASE=N -fsyntax-only repro.cpp
#include <exception>
#include <exec/any_sender_of.hpp>
#include <exec/sequence.hpp>
#include <stdexec/execution.hpp>
using namespace stdexec;
using namespace exec;
using AnyVoid = any_sender<any_receiver<
completion_signatures<set_value_t(), set_error_t(std::exception_ptr)>>>;
using AnyBool = any_sender<any_receiver<
completion_signatures<set_value_t(bool), set_error_t(std::exception_ptr)>>>;
#if CASE == 1 // OK: 2-element sequence containing a type-erased sender
auto make() { return sequence(AnyVoid(just()), AnyBool(just(true))); }
#elif CASE == 2 // ERROR: 3-element sequence containing a type-erased sender
auto make() { return sequence(just(), AnyVoid(just()), AnyBool(just(true))); }
#elif CASE == 3 // OK: 3-element sequence of concrete senders
auto make() { return sequence(just(), just(), just(true)); }
#endif
int main() { (void)make(); }
| Case |
Senders |
Result |
| 1 |
sequence(any, any) |
✅ compiles |
| 2 |
sequence(just(), any, any) |
❌ compile error |
| 3 |
sequence(just(), just(), just(true)) |
✅ compiles |
Error (CASE 2, paths shortened)
__sequence.hpp:395: error: no matching function for call to 'get_completion_signatures'
note: __has_get_completion_signatures<const exec::any_sender<…>&,
const cprop<get_domain_t,…>&> evaluated to false
note: candidate (consteval static, with env): constraints not satisfied
[_Sender = const exec::any_sender<…>&]
note: candidate (consteval static, no env): too many template arguments
Analysis
exec::sequence(s...) computes its completion signatures recursively: it peels off the first sender and re-queries the tail as a sub-sequence (__seq::__sndr<…>), which holds its children by const& (the error shows __seq::__sndr<const __sexpr&, const any_sender<…>&>). With 2 senders there is no tail recursion — the senders are queried by value; with 3+ there is.
any_sender exposes its completions via _isender::_interface_:
template <std::derived_from<_interface_> Self, class... Env>
static consteval auto get_completion_signatures();
The discovery machinery STDEXEC_GET_COMPLSIGS invokes it as
REMOVE_REFERENCE(_Sender)::template get_completion_signatures<_Sender, _Env...>(),
forwarding _Sender un-decayed as Self:
- by value →
Self = any_sender → derived_from<any_sender, _interface_> is true → works (2-element case).
- through a reference →
Self = const any_sender& → derived_from<const any_sender&, _interface_> is false (a reference type is never derived_from anything) → no viable overload, and any_sender has no ::completion_signatures typedef / env-taking member to fall back on → the whole __with discovery disjunction fails.
So a type-erased any_sender cannot have its completion signatures queried through a cv/ref-qualified type, and sequence's ≥3-arg recursion introspects its children precisely that way (const&). That's why 2 works, 3+ fails, and 3-concrete works (concrete senders don't have the derived_from<Self> constraint).
Suggested fix
Decay Self before the derived_from check — either in any_sender's constraint
(std::derived_from<std::remove_cvref_t<Self>, _interface_>) or by having the query
machinery pass a decayed sender type as Self.
Environment
- stdexec
main @ fee4d651
- clang 22.1.5, libc++,
-std=c++23, macOS arm64
AUTHOR NOTE: our recent attempted upgrade to using the latest stdexec failed badly due to the below issue, as reported by Claude. There is a workaround of nested senders, eg.
sequence(a, sequence(b, c)), which ensures each sequence is limited to two senders, but that feels a bit rubbish so we are reverting to the old version for now.Summary
A type-erased
exec::any_sendercomposes fine in a 2-elementexec::sequence, but putting it in asequenceof three or more senders fails to compile withno matching function for call to 'get_completion_signatures'. A 3-element sequence of concrete senders is fine.Reproducer
sequence(any, any)sequence(just(), any, any)sequence(just(), just(), just(true))Error (CASE 2, paths shortened)
Analysis
exec::sequence(s...)computes its completion signatures recursively: it peels off the first sender and re-queries the tail as a sub-sequence (__seq::__sndr<…>), which holds its children byconst&(the error shows__seq::__sndr<const __sexpr&, const any_sender<…>&>). With 2 senders there is no tail recursion — the senders are queried by value; with 3+ there is.any_senderexposes its completions via_isender::_interface_:The discovery machinery
STDEXEC_GET_COMPLSIGSinvokes it asREMOVE_REFERENCE(_Sender)::template get_completion_signatures<_Sender, _Env...>(),forwarding
_Senderun-decayed asSelf:Self = any_sender→derived_from<any_sender, _interface_>is true → works (2-element case).Self = const any_sender&→derived_from<const any_sender&, _interface_>is false (a reference type is neverderived_fromanything) → no viable overload, andany_senderhas no::completion_signaturestypedef / env-taking member to fall back on → the whole__withdiscovery disjunction fails.So a type-erased
any_sendercannot have its completion signatures queried through a cv/ref-qualified type, andsequence's ≥3-arg recursion introspects its children precisely that way (const&). That's why 2 works, 3+ fails, and 3-concrete works (concrete senders don't have thederived_from<Self>constraint).Suggested fix
Decay
Selfbefore thederived_fromcheck — either inany_sender's constraint(
std::derived_from<std::remove_cvref_t<Self>, _interface_>) or by having the querymachinery pass a decayed sender type as
Self.Environment
main@fee4d651-std=c++23, macOS arm64