Skip to content

[exec.snd.concepts] sender_to concept should avoid checking that the receiver's completion-methods are present #327

Open
@lewissbaker

Description

@lewissbaker

From mailing list thread http://lists.isocpp.org/lib/2025/02/30463.php on the topic of #320.

During discussion of #320, a concern was raised that adding the sender_to constraint to connect_result_t would cause the computation of the operation-state type to instantiate methods on the receiver too eagerly.

The sender_to concept currently checks that:

  • the sender is able to compute completion signatures using the receiver's environment
  • the receiver is able to accept all of the computed completion signatures overloads
  • connect() can be called with the provided sender and receiver

It is not clear to me that when checking compatibility of a sender and receiver that we should be checking that the receiver methods on the receiver are invocable for each of the completion signatures and SFINAE'ing out if not all of the receiver methods are provided.

In general, it should be a requirement on the implementer of the receiver that they can accept whatever completion signatures the sender it is being connected to can produce.
If a caller tries to connect a receiver to a sender and the receiver does not support all of the completion signatures then this is a programmer error and should result in a hard-error that points to exactly which overload is missing.
i.e. the check that the receiver supports all of the completion signatures should probably be a 'mandates' on 'connect()' rather than having it checked in 'constraints'.

I've found that it is preferable to avoid putting any constraints on the receiver's completion handlers in order to avoid some kinds of constraint recursion / cyclic template instantiation dependencies.

Checking for whether or not a given receiver's completion method can be instantiated is not necessarily a great way to test if a receiver can accept an input.
In order to put constraints on a receiver's completion methods you generally need to do some type computation for the current operation and then forward the constraint checking to the parent receiver.
If we end up checking these constraints for every call to connect() (or on connect_result_t which is also used in most places connect() is used), then you can easily end up with an exponential explosion of the number of constraints being checked for deeply nested sender expressions.
e.g. a call to connect() or connect_result_t on a top-level sender checks the constraints on its receiver, then calls into connect()/connect_result_t on a child sender, which checks the constraints on the receiver passed to that, which then forwards the checks back to the parent receiver, checking the constraint again.
This can lead to the top-level receiver having its constraints checked N times where there are N child senders nested underneath it.
It can get even worse if the receiver uses constraints to control overload resolution as this can result in a parent constraint being checked multiple times for a given child receiver constraint check and this fanning out can multiply as it propagates up the receiver tree.
This can easily result in compile times that are orders of magnitude longer than code-bases where we do not check the constraints on the receiver.

Also, in practice, many receiver methods may actually be unconstrained and so checking to see whether or not a receiver's completion function can be instantiated with a given completion signature is not a guarantee that this receiver actually supports receiving that completion signature.
It may be that the receiver method effectively has a 'mandates' clause that the completion signature is one of the ones that it supports and may end up causing a hard-error anyway further down the track.

So, while I don't have a problem with constraining the connect_result_t alias with sender_to as proposed in #320, I do think that the sender_to concept should be made to check fewer things.

In particular, the sender_to concept should be simply constraining that we can compute completion-signatures for the sender with the receiver's environment and checking that we can indeed call connect(), which is needed to compute the connect_result_t anyway, but otherwise avoiding checking the receiver's completion methods.

i.e. something like this

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

If desired, we can move these completion-function constraints to a 'mandates' clause on 'connect()'.

Further, we should avoid placing any constraints on receiver completion functions in standard-defined receivers, except where required for overload resolution, and prefer using mandates clauses instead.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions