Repository navigation
alias template connect_result_t should be constrained with sender_to #320
Description
Activity
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 thatcompletion_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 callconnect(), 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_totoconnect_result_t, but rather that thesender_toconcept is checking too much.I suggest replacing the
receiver_ofpart of thesender_inconcept with justreceiver<Rcvr>and then just require that it can compute completion signatures and thatconnect()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_valuemethod.Thoughts?
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_senderdefined 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::connectandreceiver::set_valueare 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_toconcept. 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 weakeningsender_tolike you suggest.- addedpending-wg21A paper or an LWG issue exitsA paper or an LWG issue exits
on Feb 7, 2025
since turning
execution::connect(sndr, rcvr)'s constraints to a mandates, theconnectcustomization point is now unconstrained.but there is at least one place in the algorithms ([exec.let]) that is using
connect_result_tin an immediate context of a function template with the expectation thatconnect_result_t<Sndr, Rcvr>will be well-formed only whenSndrandRcvrcan 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_talias template. just ascompletion_signatures_of_t<Sndr, Env>is constrained withsender_in<Sndr, Env>, so too shouldconnect_result_t<Sndr, Rcvr>be constrained withsender_to<Sndr, Rcvr>.for reference, the
sender_toconcept is defined as follows: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>()));