Skip to content

exec::any_sender fails get_completion_signatures inside a sequence of 3+ senders (works with 2) #2101

Description

@jessmorecroft

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_senderderived_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions