Skip to content

alias template connect_result_t should be constrained with sender_to #320

Description

@ericniebler

since turning execution::connect(sndr, rcvr)'s constraints to a mandates, the connect customization point is now unconstrained.

but there is at least one place in the algorithms ([exec.let]) that is using connect_result_t in an immediate context of a function template with the expectation that connect_result_t<Sndr, Rcvr> will be well-formed only when Sndr and Rcvr can actually be connected. with the current definition, connect_result_t<Sndr, Rcvr> could very well cause a hard error; it will never cuase a substitution failure.

the solution is to constrain the connect_result_t alias template. just as completion_signatures_of_t<Sndr, Env> is constrained with sender_in<Sndr, Env>, so too should connect_result_t<Sndr, Rcvr> be constrained with sender_to<Sndr, Rcvr>.

for reference, the sender_to concept is defined as follows:

template<class Sndr, class Rcvr>
    concept sender_to =
      sender_in<Sndr, env_of_t<Rcvr>> &&
      receiver_of<Rcvr, completion_signatures_of_t<Sndr, env_of_t<Rcvr>>> &&
      requires (Sndr&& sndr, Rcvr&& rcvr) {
        connect(std::forward<Sndr>(sndr), std::forward<Rcvr>(rcvr));
      };

Proposed resolution

Change [exec.syn] as follows:

  template<class Sndr, class Rcvr>
+     requires sender_to<Sndr, Rcvr>
    using connect_result_t =
      decltype(connect(declval<Sndr>(), declval<Rcvr>()));

Activity

  1. jwakely commented on Feb 5, 2025

    @jwakely
    Member
  2. lewissbaker commented on Feb 6, 2025

    @lewissbaker
    Collaborator

    I'm not sure that adding this constraint to the type-alias is a great idea.

    In order to avoid unnecessary instantiations and circular dependencies during template instantiation, I thought we were shying away from checking that a receiver's set_value, etc. methods were valid and instead leaning towards just checking that completion_signatures_of_t<Sndr, env_of_t<Rcvr>> was able to compute a set of completion-signatures and, if so, then it a programming error by the caller if they subsequently call connect(), passing a receiver that does not support all of the returned completion signatures.

    However, perhaps my concern isn't so much about adding the constraint of sender_to to connect_result_t, but rather that the sender_to concept is checking too much.

    I suggest replacing the receiver_of part of the sender_in concept with just receiver<Rcvr> and then just require that it can compute completion signatures and that connect() is callable.

    This should avoid unnecessarily instantiating the receiver completion methods eagerly when computing operation-state types, which if they fail, would lead to hard-to-diagnose SFINAE instead of hard-errors pointing to the line of code trying to call/instantiate the set_value method.

    Thoughts?

  3. ericniebler commented on Feb 7, 2025

    @ericniebler
    CollaboratorAuthor

    i spent some time playing with this, and there is a potential problem like what you describe in the proposed resolution. the problem happens with the test_sender defined below. (discussion continues after the break)

    namespace ex = stdexec;
    
    template <class Sender, class Receiver>
    struct opstate;
    
    template <class Sender, class Receiver>
    struct receiver {
      using receiver_concept = ex::receiver_t;
    
      template <class... Args>
      void set_value(Args&&... args) noexcept
        requires requires(opstate<Sender, Receiver>* opstate) {
          ex::set_value(std::move(opstate->rcvr_), static_cast<Args &&>(args)...);  // HERE
        }
      {
        ex::set_value(std::move(opstate_->rcvr_), static_cast<Args&&>(args)...);
      }
    
      auto get_env() const noexcept -> ex::env_of_t<Receiver> {
        return ex::get_env(opstate_->rcvr_);
      }
    
      opstate<Sender, Receiver>* opstate_;
    };
    
    template <class Sender, class Receiver>
    struct opstate {
      using operation_state_concept = ex::operation_state_t;
      Receiver rcvr_;
      ex::connect_result_t<Sender, receiver<Sender, Receiver>> child_op_;          // HERE
    
      opstate(Sender sndr, Receiver rcvr)
        : rcvr_(rcvr)
        , child_op_(ex::connect(std::move(sndr), receiver<Sender, Receiver>{this})) {
      }
    
      void start() noexcept {
        ex::start(child_op_);
      }
    };
    
    template <class Child>
    struct test_sender {
      using sender_concept = ex::sender_t;
    
      template <class Env>
      using _completions_t = ex::completion_signatures_of_t<Child, Env>;
    
      template <class Env>
      auto get_completion_signatures(const Env&) && -> _completions_t<Env> {
        return {};
      }
    
      template <class Receiver>
        requires ex::sender_to<Child, receiver<Child, Receiver>>                   // HERE
      auto connect(Receiver rcvr) && -> opstate<Child, Receiver> {
        return opstate<Child, Receiver>{std::move(child_), rcvr};
      }
    
      Child child_;
    };
    
    int main() {
      auto [sched] = ex::sync_wait(test_sender{ex::read_env(ex::get_scheduler)}).value();
    }
    

    i tried compiling this code before and after adding the constraint to connect_result_t. Without the constraint, the code compiles. With the constraint, the program is ill-formed due to constraint recursion.

    the constraints on test_sender::connect and receiver::set_value are necessary for the problem to happen. the code is not unreasonable, imo.

    in short, i agree that we should leave the constraint off of connect_result_t.

    i don't (yet) agree that we need to weaken the current sender_to concept. merely testing that the receiver has the necessary completion functions does not cause the instantiation of those functions, and i haven't yet found a situation where code that is broken would be fixed by weakening sender_to like you suggest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1bugSomething isn't workingpending-wg21A paper or an LWG issue exits

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions