| Document #: | P3557R1 |
| Date: | 2025-02-13 |
| Project: | Programming Language C++ |
| Audience: |
LEWG Library Evolution |
| Reply-to: |
Eric Niebler <eric.niebler@gmail.com> |
“Only the exceptional paths bring exceptional glories!”
— Mehmet Murat Ildan
The hardest part of writing a sender algorithm is often the computation of its completion signatures, an intricate meta-programming task. Using sender algorithms incorrectly leads to large, incomprehensible errors deep within the completion-signatures meta-program. What is needed is a way to propagate type errors automatically to the API boundary where they can be reported concisely, much the way exceptions do for runtime errors.
Support for exceptions during constant-evaluation was recently accepted into the Working Draft for C++26. We can take advantage of this powerful new feature to easily propagate type errors during the computation of a sender’s completion signatures. This significantly improves the diagnostics users are likely to encounter while also simplifying the job of writing new sender algorithms.
This paper proposes the following changes to the working draft with the addition of [P3164R3]. Subsequent sections will address the motivation and the designs in detail.
std::execution::get_completion_signatures
from a customization point object that accepts a sender and (optionally)
an environment to a consteval
function template that takes no arguments, as follows:
Before
|
After
|
|---|---|
|
|
get_completion_signatures from a
member function that accepts the cv-qualified sender object and
an optional environment object to a
static constexpr function
template that take the sender and environment types as template
parameters.
Before
|
After
|
|---|---|
|
|
sender_in<Sender, Env...>
concept to test that get_completion_signatures<Sndr, Env...>()
is a constant expression.
Before
|
After
|
|---|---|
|
|
In the exposition-only
basic-sender class
template, specify under what conditions its
get_completion_signatures static
member function is ill-formed when called without an
Env template parameter (see
proposed wording for details).
Add a dependent_sender
concept that is modeled by sender types that do not know how they will
complete independent of their execution environment.
[Optional]: Remove the
transform_completion_signatures
alias template.
The following additions are suggested by this paper to make working
with completion signatures in
constexpr code easier. None of
these additions is strictly necessary.
Extend the completion_signatures
class template with constexpr
operations that make the manipulation of completion signatures more
ergonomic. These extensions are:
Combining two sets of completion signatures with
operator+. Alternative: provide
a variadic
concat_completion_signatures
function.
Treating an instance of a completion signatures specialization as
a tuple of function pointers. For example, completion_signatures<set_value_t(int), set_stopped_t()>{}
would be usable as if it were tuple<set_value_t(*)(), set_stopped_t(*)()>{nullptr, nullptr},
making std::apply useful for
manipulating completion signatures.
Adding CTAD to
completion_signatures to deduce
the signature types from a list of function pointers. Together with the
above item, the following is the identity transform, given a
completion_signatures object
cs:
auto cs2 = std::apply([](auto... sigs){ return completion_signatures{sigs...}; }, cs); static_assert(std::same_as<decltype(cs), decltype(cs2)>);
Add a make_completion_signatures
helper function that takes signatures as template arguments or as
function arguments or both. The returned value would have signatures
that have been normalized, sorted ([P2830R7]), and made unique.
Add a get_child_completion_signatures
function template that makes it easy for a sender adaptor to get the
completion signatures of the child sender with the proper cv
qualification. It is simply:
template <class Parent, class Child, class... Env> consteval auto get_child_completion_signatures() { using CvChild = decltype(std::forward_like<Parent>(declval<Child&>())); return get_completion_signatures<CvChild,FWD-ENV(Env)...>(); }
Reintroduce transform_completion_signatures
as a constexpr function template
that accepts lambdas as transforms. For example, the following code
removes all error completions from a set,
cs:
auto cs2 = transform_completion_signatures( cs, {}, // accept the default value completion transform []<class Error>() { return completion_signatures(); });
See transform_completion_signatures
for a design description and reference implementation of the
transform_completion_signatures
function template.
Add an invalid_completion_signature
consteval function template
whose return type is
completion_signatures<>
but that throws an unspecified exception type that contains diagnostic
information. The read_env sender
might use it as follows:
template <class Q> template <class Self, class Env> static constexpr auto read_env_sender<Q>::get_completion_signatures() { namespace exec = std::execution; if constexpr (!std::invocable<Q, Env>) { // return type deduced to be completion_signatures<> but function exits // with an exception that contains the relevant diagnostic information. return exec::invalid_completion_signature< _IN_ALGORITHM<read_env>, _WITH_QUERY(Q), _WITH_ENVIRONMENT(Env) >("The environment does not provide a value for the given query type."); } else { using Result = std::invoke_result_t<Q, Env>; return exec::completion_signatures<exec::set_value_t(Result)>(); } }
Since R0, a significant fraction of C++26’s
std::execution has been
implemented with the design changes proposed by this paper. Several bugs
in R0 have been found and fixed as a result.
In addition, this paper exposed several bugs in the Working Draft for C++26. As those bugs relate to the computation of completion signatures, R1 integrates the proposed fixes for those bugs.
std::execution::get_completion_signatures
which was the victim of an incomplete last-minute edit.get_completion_signatures.transform_completion_signatures
to take a transform function for the stopped completion signature
instead of a
completion_signatures
object.impls-for<Tag>::check-types
customizations.basic-sender<Tag>::get_completion_signatures.std::execution::connect.stopped_as_optional.transform_sender(sndr, env).let_*,
schedule_from,
into_variant,
split, and
when_all algorithms when the
prececessor’s result datums cannot be copied into intermediate
storage.eptr_completion_ifmake_completion_signaturestransform_completion_signatures
(a constexpr function
version)invalid_completion_signaturestopped_as_optional tests
constraints satisfaction of self instead of child”.FWD-ENV as they
should”.as-sndr2(Sig)
in [exec.let] is incomplete”.let_[*].transform_env is
specified in terms of the let_*
sender itself instead of its child”.This paper exists principly to improve the experience of users who make type errors in their sender expressions by leveraging exceptions during constant- evaluation. It is a follow-on of [P3164R2], which defines a category of “non-dependent” senders that can and must be type-checked early.
Senders have a construction phase and a subsequent connection phase. Prior to P3164, all type-checking of senders happened at the connection phase (when a sender is connected to a receiver). P3164 mandates that the sender algorithms type-check non-dependent senders, moving the diagnostic closer to the source of the error.
This paper addresses the quality of those diagnostics and the diagnostics users encounter when a dependent sender fails type-checking at connection time.
Senders are expression trees, and type errors can happen deep within their structure. If programmed naively, ill-formed senders would generate megabytes of incomprehensible diagnostics. The challenge is to report type errors concisely and comprehensibly, at the right level of abstraction.
Doing this requires propagating domain-specific descriptions of type errors out of the completion signatures meta-program so they can be reported concisely. Such error detection and propagation is very cumbersome in template meta-programming.
The C++ solution to error propagation is exceptions. With the
adoption of [P3068R6], C++26 has
gained the ability to throw and catch exceptions during
constant-evaluation. If we express the computation of completion
signatures as a constexpr
meta-program, we can use exceptions to propagate type errors. This
greatly improves diagnostics and even simplifies the code that computes
completion signatures.
This paper proposes changes to
std::execution that make the
computation of a sender’s completion signatures an evaluation of a
constexpr function. It also
specifies the conditions under which the computation is to exit with an
exception.
get_completion_signaturesIn the Working Draft, a sender’s completion signatures are determined
by the type of the expression std::execution::get_completion_signatures(sndr, env)
(or, after P3164, std::execution::get_completion_signatures(sndr)
for non-dependent senders). Only the type of the expression matters; the
expression itself is never evaluated.
In the design proposed by this paper, the
get_completion_signatures
expression must be constant-evaluated in order use exceptions to report
errors. To make it ammenable to constant evaluation, it must not accept
arguments with runtime values, so the expression is changed to std::execution::get_completion_signatures<Sndr, Env...>(),
where get_completion_signatures
is a consteval function.
If an unhandled exception propagates out of
get_completion_signatures the
program is ill-formed (because
get_completion_signatures is
consteval). The diagnostic
displays the type and value of the exception.
std::execution::get_completion_signatures<Sndr, Env...>()
in turn calls remove_reference_t<Sndr>::template
get_completion_signatures<Sndr, Env...>(), which computes
the completion signatures or throws as appropriate, as shown below:
namespace exec = std::execution; struct void_sender { using sender_concept = exec::sender_t; template <class Self, class... Env> static constexpr auto get_completion_signatures() { return exec::completion_signatures<exec::set_value_t()>(); } /* … more … */ };
To better support the
constexpr value-oriented
programming style, calls to
get_completion_signatures from a
constexpr function are never
ill-formed, and they always have a
completion_signatures type.
get_completion_signatures
reports errors by failing to be a constant expression.
[P3164R3] introduces the concept of
non-dependent senders: senders that have the same completion signatures
regardless of the receiver’s execution environment. For a sender type
DependentSndr whose completions
do depend on the environment, what should happen when the
sender’s completions are queried without an environment? That is, what
should the semantics be for get_completion_signatures<DependentSndr>()?
get_completion_signatures<DependentSndr>()
should follow the general rule: it should be well-formed in a
constexpr function, and it
should have a
completion_signatures type. That
way, sender adaptors do not need to do anything special when computing
the completions of child senders that are dependent. So get_completion_signatures<DependentSndr>()
should throw.
If get_completion_signatures<Sndr>()
throws for dependent senders, and it also throws for non-dependent
senders that fail to type-check, how then do we distinguish between
valid dependent and invalid non-dependent senders? We can distinguish by
checking the type of the exception.
An example will help. Consider the
read_env(q) sender, a dependent
sender that sends the result of calling
q with the receiver’s
environment. It cannot compute its completion signatures without an
environment. The natural way for the
read_env sender to express that
is to require an Env parameter
to its customization of
get_completion_signatures:
namespace exec = std::execution; template <class Query> struct read_env_sender { using sender_concept = exec::sender_t; template <class Self, class Env> // NOTE: Env is not optional! static constexpr auto get_completion_signatures() { if constexpr (!std::invocable<Query, Env>) { throwexception-type-goes-here(); } else { using Result = std::invoke_result_t<Query, Env>; return exec::completion_signatures<exec::set_value_t(Result)>(); } } /* … more … */ };
That makes read_env_sender<Q>::get_completion_signatures<Sndr>()
an ill-formed expression, which the
get_completion_signatures
function can detect. In such cases, it would throw an exception of a
special type that it can catch later when distinguishing between
dependent and non-dependent senders.
Since the design has several parts, reading the implementation of
get_completion_signatures is
probably the easiest way to understand it. The implementation is shown
below with comments describing the parts.
// This macro expands to an invocation of SNDR's get_completion_signatures // customization. #define GET_COMPLSIGS(SNDR, ...) std::remove_reference_t<SNDR>::template \ get_completion_signatures<SNDR __VA_OPT__(,) __VA_ARGS__>() // This macro expands to an evaluation of EXPR, followed by an invocation // of the _checked_complsigs function which validates its type. #define CHECK_COMPLSIGS(EXPR) (EXPR, _check_complsigs<decltype(EXPR)>()) template <class Sndr> using _nested_complsigs_t = std::remove_reference_t<Sndr>::completion_signatures; // This helper ensures that the passed type is indeed a specialization of the // completion_signatures class template, and throws if it is not. template <class Completions> consteval auto _check_complsigs() { if constexpr (_valid_completion_signatures<Completions>) // We got a type that is a specialization of the completion_signatures // class template representing the sender's completions. Return it. return Completions(); else // invalid_completion_signature throws unconditionally. Its return type // is `completion_signatures<>`, which prevents downstream errors that // would occur in other customizations of `get_completion_signatures` // if computing the completions of a child returned an object with a // surprising type. return invalid_completion_signature<unspecified>(unspecified); } template <class Sndr, class... Env> consteval auto _get_completion_signatures_helper() { // The following `if` tests whether GET_COMPLSIGS(Sndr, Env...) // is a well-formed expression. if constexpr (requires { GET_COMPLSIGS(Sndr, Env...); }) { // The GET_COMPLSIGS(Sndr, Env...) expression is well-formed, but it may // throw an exception or otherwise fail to be a constant expression. // By evaluating it, we cause its exception and its non-constexpr-ness to // propagate. Then CHECK_COMPLSIGS ensures that its type is indeed // a specialization of the completion_signatures class template. return CHECK_COMPLSIGS(GET_COMPLSIGS(Sndr, Env...)); } // The following `if` does the same as above, but for GET_COMPLSIGS(Sndr). // A non-dependent sender may announce itself by way of this signature. else if constexpr (requires { GET_COMPLSIGS(Sndr); }) { // Same as above: propagate any exceptions and non-constexpr-ness, and // verify the expression has the right type. return CHECK_COMPLSIGS(GET_COMPLSIGS(Sndr)); } // Test whether Sndr has a nested ::completion_signatures type alias: else if constexpr (requires { _nested_complsigs_t<Sndr>(); }) { // It has the nested type alias, but does it denote a specialization of // the completion_signatures class template? return CHECK_COMPLSIGS(_nested_complsigs_t<Sndr>()); } // If none of the above expressions are well-formed, then we don't know // the sender's completions. If we are testing without an environment, then // we assume Sndr is a dependent sender. Throw an exception that // communicates that. else if constexpr (sizeof...(Env) == 0) { return (throwdependent-sender-error(), completion_signatures()); } else { // We cannot compute the completion signatures for this sender and // environment. Give up and throw an exception. return invalid_completion_signature<unspecified>(unspecified); } } template <class Sndr> consteval auto get_completion_signatures() -> _valid_completion_signatures auto { // There is no environment, which means we are asking for the sender's non- // dependent completion signatures. If the sender is dependent, this will // exit with a special exception type. return _get_completion_signatures_helper<Sndr>(); } template <class Sndr, class Env> consteval auto get_completion_signatures() -> _valid_completion_signatures auto { // Apply a lazy sender transform if one exists before computing the completion signatures: using Domain = decltype(_get_domain_late(std::declval<Sndr>(), std::declval<Env>())); using NewSndr = decltype(transform_sender(Domain(), std::declval<Sndr>(), std::declval<Env>())); return _get_completion_signatures_helper<NewSndr, Env>(); }
Given this definition of
get_completion_signatures, we
can implement a dependent_sender
concept as follows:
// Returns true when get_completion_signatures<Sndr>() throws a // dependent-sender-error. Returns false when // get_completion_signatures<Sndr>() returns normally (Sndr is non-dependent), // or when it throws any other kind of exception (Sndr fails type-checking). template <class Sndr> consteval boolis-dependent-sender-helper() try { (void) get_completion_signatures<Sndr>(); return false; } catch (dependent-sender-error&) { return true; } template <class Sndr> concept dependent_sender = sender<Sndr> && std::bool_constant<is-dependent-sender-helper<Sndr>()>::value;
After the adoption of [P3164R3], the sender
algorithms are all required to return senders that are either dependent
or else that type-check successfully. This paper proposes adding that
type-checking as a Mandates on the exposition-only
make-sender function
template that all the algorithms use to construct their return
value.
Users who define their own sender algorithms can use
dependent_sender and
get_completion_signatures to
perform early type-checking of their own sender types using a helper
such as the following:
template <class Sndr> constexpr auto _type_check_sender(Sndr sndr) { if constexpr (!dependent_sender<Sndr>) { // This line will fail to compile if Sndr fails its type checking. We // don't want to perform this type checking when Sndr is dependent, though. // Without an environment, the sender doesn't know its completions. (void) get_completion_signatures<Sndr>(); } return sndr; }
Using this helper, a then
algorithm might type-check its returned senders as follows:
inline constexpr struct then_t { template <sender Sndr, class Fn> auto operator()(Sndr sndr, Fn fn) const { return _type_check_sender(_then_sender{std::move(sndr), std::move(fn)}); } } then {};
sender_inWith the above changes, we need to tweak the
sender_in concept to require
that get_completion_signatures<Sndr, Env...>()
is a constant expression.
The changes to sender_in
relative to [P3164R3] are as
follows:
template <auto>concept is-constant = true; // exposition onlytemplate<class Sndr, class... Env> concept sender_in = sender<Sndr> && (sizeof...(Env) <= 1) (queryable<Env> &&...) &&is-constant<get_completion_signatures<Sndr, Env...>()>;requires (Sndr&& sndr, Env&&... env) {{ get_completion_signatures(std::forward<Sndr>(sndr), std::forward<Env>(env)...) }-> valid-completion-signatures;};
basic-senderThe sender algorithms are expressed in terms of the exposition-only
class template
basic-sender. The
mechanics of computing completion signatures is not specified, however,
so very little change there is needed to implement this proposal.
We do, however, have to say when
basic-sender::get_completion_signatures<Sndr>()
is ill-formed. In [P3164R3],
non-dependent senders are dealt with by discussing whether or not a
sender’s potentially-evaluated completion operations are dependent on
the type of the receiver’s environment. In this paper, we make a similar
appeal when specifying whether or not
basic-sender::get_completion_signatures<Sndr>()
is well-formed.
dependent_senderUsers who write their own sender adaptors will also want to perform early type-checking of senders that are not dependent. Therefore, they need a way to determine whether or not a sender is dependent.
In the section get_completion_signatures
we show how the concept
dependent_sender can be
implemented in terms of this paper’s
get_completion_signatures
function template. By making this a public-facing concept, we give
sender adaptor authors a way to do early type-checking, just like the
standard adaptors.
completion_signaturesComputing completions signatures is now to be done using
constexpr meta-programming by
manipulating values using ordinary imperative C++ rather than template
meta-programming. To better support this style of programming, it is
helpful to add constexpr
operations that manipulate instances of specializations of the
completion_signatures class
template.
For example, it should be possible to take the union of two sets of
completion signatures. operator+
seems like a natural choice for that:
completion_signatures<set_value_t(int), set_error_t(exception_ptr)> cs1; completion_signatures<set_stopped_t(), set_error_t(exception_ptr)> cs2; auto cs3 = cs1 + cs2; // completion_signatures<set_value_t(int), // set_error_t(exception_ptr), // set_stopped_t()>
operator+ is nice because it
is possible to do a variadic fold over a set of
completion_signatures objects to
concatenate them all:
[](auto... cs) { // a pack of completion_signatures objects return completion_signatures() +...+ cs; }
A different option instead of
operator+ would be to use a free
function that takes a variable number of
completion_signature objects and
concatenates all of them.
It can also be convenient for
completion_signatures
specializations to model tuple-like.
Although tuple elements cannot have funtion type, they can have function
pointer type. With this proposal, an object like completion_signatures<set_value_t(int), set_stopped_t()>{}
behaves like tuple<set_value_t(*)(int), set_stopped_t(*)()>{nullptr, nullptr}
(except that it wouldn’t actually have to store the
nullptrs). That would make it
possible to manipulate completion signatures using
std::apply:
auto cs = /* … */; // Add an lvalue reference to all arguments of all signatures: auto add_ref = []<class T, class... As>(T(*)(As...)) -> T(*)(As&...) { return {}; }; auto add_ref_all = [=](auto... sigs) { return make_completion_signatures(add_ref(sigs)...); }; return std::apply(add_ref_all, cs);
The code above uses another nice-to-have feature: a
make_completion_signatures
helper function that deduces the signatures from the arguments, removes
any duplicates, and returns a new instance of
completion_signatures.
Consider trying to do all the above using template meta-programming. 😬
make_completion_signaturesThe
make_completion_signatures
helper function described just above would allow users to build a
completion_signatures object
from a bunch of signature types, or from function pointer objects, or a
combination of both:
// Returns a default-initialized object of type completion_signatures<Sigs...>, // where Sigs is the set union of the normalized ExplicitSigs and DeducedSigs. template <completion-signature... ExplicitSigs,completion-signature... DeducedSigs> constexpr auto make_completion_signatures(DeducedSigs*... sigs) noexcept ->valid-completion-signaturesauto;
To “normalize” a completion signature means to strip rvalue
references from the arguments. So, set_value_t(int&&, float&)
becomes
set_value_t(int, float&).
make_completions_signatures
first normalizes all the signatures and then removes duplicates. ([P2830R7] lets us order types, so making
the set unique will be O(n log n).)
transform_completion_signaturesThe current Working Draft has a utility to make type transformations
of completion signature sets simpler: the alias template
transform_completion_signatures.
It looks like this:
template <class... As> using value-transform-default = completion_signatures<set_value_t(As...)>; template <class Error> using error-transform-default = completion_signatures<set_error_t(Error)>; template <valid-completion-signatures Completions, valid-completion-signatures OtherCompletions = completion_signatures<>, template <class...> class ValueTransform = value-transform-default, template <class> class ErrorTransform = error-transform-default, valid-completion-signatures StoppedCompletions = completion_signatures<set_stopped_t()>> using transform_completion_signatures = /*see below*/;
Anything that can be done with
transform_completion_signatures
can be done in constexpr using
std::apply, a lambda with
if constexpr, and
operator+ of
completion_signatures objects.
In fact, we could even implement
transform_completion_signatures
itself that way:
template </* … as before … */> using transform_completion_signatures = std::constant_wrapper< // see [@P2781R5] std::apply( [](auto... sigs) { return ([]<class T, class... As>(T (*)(As...)) { if constexpr (^^T == ^^set_value_t) { // use reflection to test type equality return ValueTransform<As...>(); } else if constexpr (^^T == ^^set_error_t) { return ErrorTransform<As...[0]>(); } else { return StoppedCompletions(); } }(sigs) +...+ completion_signatures()); }, Completions() ) + OtherCompletions() >::value_type;
This paper proposes dropping the
transform_completion_signatures
type alias since it is not in the ideal form for
constexpr meta-programming, and
since std::apply is good enough
(sort of).
However, should we decide to keep the functionality of
transform_completion_signatures,
we can reexpress it as a
constexpr function that accepts
transforms as lambdas:
constexpr auto value-transform-default = []<class... As>() { return completion_signatures<set_value_t(As...)>(); }; constexpr auto error-transform-default = []<class Error>() { return completion_signatures<set_error_t(Error)>(); }; constexpr auto stopd-transform-default = [] { return completion_signatures<set_stopped_t()>(); }; template <valid-completion-signatures Completions, class ValueTransform = decltype(value-transform-default), class ErrorTransform = decltype(error-transform-default),callableStopdTransform = decltype(stopd-transform-default), valid-completion-signatures OtherCompletions = completion_signatures<>> consteval auto transform_completion_signatures(Completions completions, ValueTransform value_transform = {}, ErrorTransform error_transform = {}, StopdTransform stopd_transform = {}, OtherCompletions other_completions = {}) -> valid-completion-signatures auto;
The above form of
transform_completion_signatures
is more natural to use from within a
constexpr function. It also
makes it simple to accept the default for some arguments as shown
below:
// Transform just the error completion signatures: auto cs2 = transform_completion_signatures(cs, {}, []<class E>() { return /* … */; }); // ^^ Accept the default value transform
Since accepting the default transforms is simple, we are able to move
the infrequently used
OtherCompletions argument to the
end of the argument list.
A faithful mapping of the old alias template to the new function
template would take a
completion_signatures object for
the stopped completions instead of a stopped transform function. After
all, what is the point of a nullary callable that returns a
completion_signatures object
when you could just pass the object itself directly? The reason is
because the stopped transform function might want to throw an exception.
That is why the
transform_completion_signatures
function template takes 3 transform functions instead of two functions
and a completion_signatures
object.
Although the signature of this
transform_completion_signatures
function looks frightful, the implementation is quite straightforward,
and seeing it might make it less scary:
// A concept that is modeled by lambdas like the following: // []<class... Ts>() { return completion_signatures<...>(); } template <class Fn, class... As> concept __meta_transform_with = requires (const Fn& fn) { { fn.template operator()<As...>() } -> __valid_completion_signatures; }; template <class... As, class Fn> consteval auto __apply_transform(const Fn& fn) { if constexpr (sizeof...(As) == 0) return fn(); else if constexpr (__meta_transform_with<Fn, As...>) return fn.template operator()<As...>(); else return invalid_completion_signature< … >( … ); // see below } template < /* … as shown above … */ > consteval auto transform_completion_signatures(Completions completions, ValueTransform value_transform, ErrorTransform error_transform, StopdTransform stopd_transform, OtherCompletions other_completions) { auto transform1 = [=]<class T, class... As>(Tag(*)(As...)) { if constexpr (Tag() == set_value) // see "Completion tag comparison" below return __apply_transform<As...>(value_transform); else if constexpr (Tag() == set_error) return __apply_transform<As...>(error_transform); else return __apply_transform<As...>(stopd_transform); }; auto transform_all = [=](auto*... sigs) { return (transform1(sigs) +...+ completion_signatures()); }; return std::apply(transform_all, completions) + other_completions; }
Like
get_completion_signatures,
transform_completion_signatures
always returns a specialization of
completion_signatures and
reports errors by throwing exceptions. It expects the lambdas passed to
it to do likewise (but handles it gracefully if they don’t).
invalid_completion_signatureThe reason for the design change is to permit the reporting of type
errors using exceptions. Let’s look at an example where it would be
desirable to throw an exception from
get_completion_signatures: the
then algorithm. We will use this
example to motivate the rest of the design changes.
The then algorithm attaches a
continuation to an async operation that executes when the operation
completes successfully. With this proposal, a
then_sender’s
get_completion_signatures
customization might be implemented as follows:
template <class Sndr, class Fun> template <class Self, class... Env> constexpr auto then_sender<Sndr, Fun>::get_completion_signatures() { // compute the completions of the (properly cv-qualified) child: using Child = decltype(std::forward_like<Self>(declval<Sndr&>())); auto child_completions = get_completion_signatures<Child, decltype(FWD-ENV(declval<Env>()))...>(); // This lambda is used to transform value completion signatures: auto value_transform = []<class... As>() { if constexpr (std::invocable<Fun, As...>) { using Result = std::invoke_result_t<Fun, As...>; return completion_signatures<set_value_t(Result)>(); } else { // Oh no, the user made an error! Tell them about it. throwsome-exception-object; } }; // Transform just the value completions: return transform_completion_signatures(child_completions, value_transform); }
We would like to make it dead simple to throw an exception that will convey a domain-specific diagnostic to the user. That way, the authors of sender algorithms will be more likely to do so.
The
invalid_completion_signature
helper function is designed to make generating meaningful diagnostics
easy. As an example, here is how the
then_sender’s
completion_signatures
customization might use it:
template <const auto&> struct IN_ALGORITHM; template <class Sndr, class Fun> template <class Self, class... Env> constexpr auto then_sender<Sndr, Fun>::get_completion_signatures() { /* … */ // This lambda is used to transform value completion signatures: auto value_transform = []<class... As>() { if constexpr (std::invocable<Fun, As...>) { using Result = std::invoke_result_t<Fun, As...>; return completion_signatures<set_value_t(Result)>(); } else { // Oh no, the user made an error! Tell them about it. return invalid_completion_signature< IN_ALGORITHM<std::execution::then>, struct WITH_FUNCTION(Fun), struct WITH_ARGUMENTS(As...) >("The function passed to std::execution::then is not callable " "with the values sent by the predecessor sender."); } }; /* … */ }
When the user of then makes a
mistake, say like with the expression
“just(42) | then([]() {…})”,
they will get a helpful diagnostic like the following (relevant bits
highlighted):
<source>:658:3: error: call to immediate function 'operator|<just_sender<int>>' is not a constant expression 658 | just(42) | then([](){}) | ^ <source>:564:14: note: 'operator|<just_sender<int>>' is an immediate function be cause its body contains a call to an immediate function '__type_check_sender<the n_sender<just_sender<int>, (lambda at <source>:658:19)>>' and that call is not a constant expression 564 | return __type_check_sender(then_sender{{}, self.fn_, sndr}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ <source>:358:11: note: unhandled exception of type '__sender_type_check_failure< const char *, IN_ALGORITHM<then>, WITH_FUNCTION ((lambda at <source>:658:19)), W ITH_ARGUMENTS (int)>' with content {&"The function passed to std::execution::the n is not callable with the values sent by the predecessor sender."[0]} thrown fr om here 358 | throw __sender_type_check_failure<Values...[0], What...>(values...); | ^ 1 error generated. Compiler returned: 1
The above is the complete diagnostic, regardless of how deeply nested the type error is. So long, megabytes of template spew!
Lambdas passed to
transform_completion_signatures
should return a
completion_signatures
specialization (although
transform_completion_signatures
recovers gracefully when they do not). The return type of
invalid_completion_signature is
completion_signatures<>.
By “returning” the result of calling
invalid_completion_signature,
the deduced return type of the lambda is a
completion_signatures type, as
it should be.
A possible implementation of the
invalid_completion_signature
function is shown below:
template <class... What, class... Args> struct sender-type-check-failure : std::exception { // exposition only constexpr sender-type-check-failure(Args... args) : args_{std::move(args)...} {} constexpr char const* what() const noexcept override { return unspecified; }; std::tuple<Args...> args_; // exposition only }; template <class... What, class... Args> [[noreturn, nodiscard]] consteval completion_signatures<> invalid_completion_signature(Args... args) { throw sender-type-check-failure<What..., Args...>{std::move(args)...}; }
get_child_completion_signaturesIn the then_sender above,
computing a child sender’s completion signatures is a little
awkward:
// compute the completions of the (properly cv-qualified) child: using Child = decltype(std::forward_like<Self>(declval<Sndr&>())); auto child_completions = get_completion_signatures<Child, decltype(FWD-ENV(declval<Env>()))...>();
Computing the completions of child senders will need to be done by
every sender adaptor algorithm. We can make this simpler with a
get_child_completion_signatures
helper function:
// compute the completions of the (properly cv-qualified) child: auto child_completions = get_child_completion_signatures<Self, Sndr, Env...>();
… where
get_child_completion_signatures
is defined as follows:
template <class Parent, class Child, class... Env> consteval auto get_child_completion_signatures() { using cvref-child-type = decltype(std::forward_like<Parent>(declval<Child&>())); return get_completion_signatures<cvref-child-type, decltype(FWD-ENV(declval<Env>()))...>(); }
For convenience, we can make the completion tag types equality-comparable with each other. When writing sender adaptor algorithms, code like the following will be common:
[]<class Tag, class... Args>(Tag(*)(Args...)) { if constexpr (std::is_same_v<Tag, exec::set_value_t>) { // Do something } else { // Do something else } }
Although certainly not hard, with reflection the tag type comparison becomes a litte simpler:
if constexpr (^^Tag == ^^exec::set_value_t>) {
We can make this even easier by simply making the completion tag types equality-comparable, as follows:
if constexpr (Tag() == exec::set_value) {
The author finds that this makes his code read better. Tag types would compare equal to themselves and not-equal to the other two tag types.
eptr_completion_ifThe following is a trivial utility that the author finds he uses
surprisingly often. Frequently an async operation can complete
exceptionally, but only under certain conditions. In cases such as
those, it is necessary to add a
set_error_t(std::exception_ptr)
signature to the set of completions, but only when the condition is
met.
This is made simpler with the following variable template:
template <bool PotentiallyThrowing> inline constexpr auto eptr_completion_if = std::conditional_t<PotentiallyThrowing, completion_signatures<set_error_t(exception_ptr)>, completion_signatures<>>();
Below is an example usage, from the
then sender:
template <class Sndr, class Fun> template <class Self, class... Env> constexpr auto then_sender<Sndr, Fun>::get_completion_signatures() { auto cs = get_child_completion_signatures<Self, Sndr, Env...>(); auto value_fn = []<class... As>() { /* … as shown in section "invalid_completion_signature" */ }; constexpr bool nothrow = /* … false if Fun can throw for any set of the predecessor's values */; // Use eptr_completion_if here as the "extra" set of completions that // will be added to the ones returned from the transforms. return transform_completion_signatures(cs, value_fn, {}, {}, eptr_completion_if<!nothrow>); }
Assuming we want to change how completion signatures are computed as proposed in this paper, the author would appreciate LEWG’s feedback about the suggested additions.
Do we want to use
operator+ to join two
completion_signatures
objects?
Do we want to make completion_signatures<Sigs...>
tuple-like (where completion_signatures<Sigs...>()
behaves like
tuple<Sigs*...>())?
Should we drop the
transform_completion_signatures
alias template?
Should we add a
make_completion_signatures
helper function that returns an instance of a
completion_signatures type with
its function types normalized and made unique?
Should we replace the
transform_completion_signatures
alias template with a consteval
function that does the same thing but for values?
Do we want the
invalid_completion_signature
helper function to make it easy to generate good diagnostics when
type-checking a sender fails.
Do we want the
get_child_completion_signatures
helper function to make is easy for sender adaptors to get a (properly
cv-qualified) child sender’s completion signatures?
Do we want to make the completion tag types
(set_value_t, etc.)
constexpr equality-comparable
with each other?
Do we want the
eptr_completion_if variable
template, which is an object of type completion_signatures<set_error_t(std::exception_ptr)>
or completion_signatures<>
depending on a bool template
parameter?
A significant fraction of
std::execution has been
implemented with this design change. It can be found on Compiler Explorer1 and in this
GitHub gist2. This implementation includes all
the design elements that would stress the completion signature
computation including:
Both dependent and non-dependent senders and sender adaptors, with tests for both early and late diagnosis of type errors.
Transforming completion signatures with the proposed
transform_completion_signatures
function template
The exposition-only
basic-sender has been
implemented with the changes
get_completion_signatures
customization syntax and the new
customization, which has been used to implement and type-check all the
sender types, as specified.impls-for<Tag>::check-types
A sender that is implemented via a lowering to another sender
type via a lazy transform_sender
customization
(stopped_as_optional).
Using awaitables as senders and vice versa.
[ Editor's note: This wording is relative to the current working draft with the addition of [P3164R3] ]
[ Editor's note: Change [exec.general] as follows: ]
? For function type
R(Args...), letdenote the typeNORMALIZE-SIG(R(Args...))R(whereremove-rvalue-reference-t<Args>...)remove-rvalue-reference-tis an alias template that removes an rvalue reference from a type.7 For function types
F1andF2denotingR1(Args1...)andR2(Args2...), respectively,isMATCHING-SIG(F1, F2)trueif and only ifsame_as<isR1(Args1&&...), R2(Args2&&...)NORMALIZE-SIG(F1), NORMALIZE-SIG(F2)>true.8 For a subexpression
err, letErrbedecltype((err))and letbe [ Editor's note: … as before ]AS-EXCEPT-PTR(err)
[ Editor's note: Change [execution.syn] as follows: ]
Header
<execution>synopsis [execution.syn]namespace std::execution { … as before … template<class Sndr, class... Env> concept sender_in = see below;template<class Sndr>concept dependent_sender = see below;template<class Sndr, class Rcvr> concept sender_to = see below; template<class... Ts> structtype-list; // exposition only// [exec.getcomplsigs], completion signaturesstruct get_completion_signatures_t;[ Editor's note:inline constexpr get_completion_signatures_t get_completion_signatures {};This alias is moved below and modified.]template<class Sndr, class... Env>requires sender_in<Sndr, Env...>template<class... Ts> usingusing completion_signatures_of_t =call-result-t<get_completion_signatures_t, Sndr, Env...>;decayed-tuple= tuple<decay_t<Ts>...>; // exposition only … as before … // [exec.util], sender and receiver utilities // [exec.util.cmplsig]completion signaturestemplate<class Fn> conceptcompletion-signature=see below; // exposition only template<completion-signature... Fns> struct completion_signatures; template<class Sigs> concept{}valid-completion-signatures=see below; // exposition onlystructdependent-sender-error: exception {};// exposition only// [exec.getcomplsigs]template<class Sndr, class... Env>consteval auto get_completion_signatures() ->valid-completion-signaturesauto;template<class Sndr, class... Env>requires sender_in<Sndr, Env...>using completion_signatures_of_t = decltype(get_completion_signatures<Sndr, Env...>());// [exec.util.cmplsig.trans]template<valid-completion-signaturesInputSignatures,valid-completion-signaturesAdditionalSignatures = completion_signatures<>,template<class...> class SetValue =see below,template<class> class SetError =see below,valid-completion-signaturesSetStopped = completion_signatures<set_stopped_t()>>using transform_completion_signatures = completion_signatures<see below>;template<sender Sndr,class Env = env<>,valid-completion-signaturesAdditionalSignatures = completion_signatures<>,template<class...> class SetValue =see below,template<class> class SetError =see below,valid-completion-signaturesSetStopped = completion_signatures<set_stopped_t()>>requires sender_in<Sndr, Env>using transform_completion_signatures_of =transform_completion_signatures<completion_signatures_of_t<Sndr, Env>,// [exec.run.loop], run_loop class run_loop; … as before … }AdditionalSignatures, SetValue, SetError, SetStopped>;
[ Editor's note: Add the following paragraph after [execution.syn] para 3 (this is moved from [exec.snd.concepts]) ]
? A type models the exposition-only concept
valid-completion-signaturesif it denotes a specialization of thecompletion_signaturesclass template.
[ Editor's note: Modify [exec.snd.general] as follows: ]
1 Subclauses [exec.factories] and [exec.adapt] define customizable algorithms that return senders. Each algorithm has a default implementation. Let
sndrbe the result of an invocation of such an algorithm or an object equal to the result ([concepts.equality]), and letSndrbedecltype((sndr)). Letrcvrbe a receiver of typeRcvrwith associated environmentenvof typeEnvsuch thatsender_to<Sndr, Rcvr>istrue. For the default implementation of the algorithm that producedsndr, connectingsndrtorcvrand starting the resulting operation state ([exec.async.ops]) necessarily results in the potential evaluation ([basic.def.odr]) of a set of completion operations whose first argument is a subexpression equal torcvr. LetSigsbe a pack of completion signatures corresponding to this set of completion operations, and letCSbe the type of the expressionget_completion_signatures(sndr, env)get_completion_signatures<Sndr, Env>(). ThenCSis a specialization of the class templatecompletion_signatures([exec.util.cmplsig]), the set of whose template arguments isSigs. If none of the types inSigsare dependent on the typeEnv, then the expressionget_completion_signatures(sndr)get_completion_signatures<Sndr>()is well-formed and its type isCS. If a user-provided implementation of the algorithm that producedsndris selected instead of the default:
(1.1) Any completion signature that is in the set of types denoted by
completion_signatures_of_t<Sndr, Env>and that is not part ofSigsshall correspond to error or stopped completion operations, unless otherwise specified.(1.2) If none of the types in
Sigsare dependent on the typeEnv, thencompletion_signatures_of_t<Sndr>andcompletion_signatures_of_t<Sndr, Env>shall denote the same type.
[ Editor's note: In [exec.snd.expos], change para 2 as follows: ]
2 For a queryable object
env,is an expression whose type satisfiesFWD-ENV(env)queryablesuch that for a query objectqand a pack of subexpressionsas, the expressionis ill-formed ifFWD-ENV(env).query(q, as...)forwarding_query(q)isfalse; otherwise, it is expression-equivalent toenv.query(q, as...). The typeisFWD-ENV-T(Env)decltype(.FWD-ENV(declval<Env>()))
[ Editor's note: In [exec.snd.expos], insert the following paragraph after para 22 and before para 23 (moving the exposition-only alias template out of para 24 and into its own para so it can be used from elsewhere): ]
23 Let
valid-specializationbe the following alias template:template<template<class...> class T, class... Args> conceptvalid-specialization= requires { typename T<Args...>; }; // exposition only
[ Editor's note: In
[exec.snd.expos] para 23 add the mandate below, and in para 24, change
the definition of the exposition-only
basic-sender as
follows: ]
template<class Tag, class Data =see below, class... Child> constexpr automake-sender(Tag tag, Data&& data, Child&&... child);23 Mandates: The following expressions are
true:
(23.4)
dependent_sender<Sndr> || sender_in<Sndr>, whereSndrisas defined below.basic-sender<Tag, Data, Child...>Recommended practice: When this mandate fails because
get_completion_signatures<Sndr>()would exit with an exception, implementations are encouraged to include information about the exception in the resulting diagnostic.
24 Returns: A prvalue of type
that has been direct-list-initialized with the forwarded arguments, wherebasic-sender<Tag, decay_t<Data>, decay_t<Child>...>basic-senderis the following exposition-only class template except as noted below.namespace std::execution { template<class Tag> conceptcompletion-tag= // exposition only same_as<Tag, set_value_t> || same_as<Tag, set_error_t> || same_as<Tag, set_stopped_t>;template<template<class...> class T, class... Args>structconceptvalid-specialization= requires { typename T<Args...>; }; // exposition onlydefault-impls{ // exposition only static constexpr autoget-attrs=see below; static constexpr autoget-env=see below; static constexpr autoget-state=see below; static constexpr autostart=see below; static constexpr autocomplete=see below;template<class Sndr, class... Env>static constexpr void}; … as before …check-types();template <class Sndr>usingtemplate <class Sndr, size_t I = 0> usingdata-type= decltype(declval<Sndr>().templateget<1>()); // exposition onlychild-type= decltype(declval<Sndr>().templateget<I+2>()); // exposition only … as before …template<class Sndr, class... Env>template<class Tag, class Data, class... Child> structusingcompletion-signatures-for=see below; // exposition onlybasic-sender:product-type<Tag, Data, Child...> { // exposition only using sender_concept = sender_t; usingindices-for= index_sequence_for<Child...>; // exposition only decltype(auto) get_env() const noexcept { auto& [_, data, ...child] = *this; returnimpls-for<Tag>::get-attrs(data, child...); } template<decays-to<basic-sender> Self, receiver Rcvr> auto connect(this Self&& self, Rcvr rcvr) noexcept(see below) ->basic-operation<Self, Rcvr> { return {std::forward<Self>(self), std::move(rcvr)}; } template<decays-to<basic-sender> Self, class... Env>static constexprauto get_completion_signatures();(this Self&& self, Env&&... env) noexcept->completion-signatures-for<Self, Env...> {return {};}; }}
[ Editor's note: In [exec.snd.expos], replace para 39 with the paragraphs shown below and renumber subsequent paragraphs: ]
39 Let
Sndrbe a (possiblyconst-qualified) specializationbasic-senderor an lvalue reference of such, letRcvrbe the type of a receiver with an associated environment of typeEnv. If the typeis well-formed, letbasic-operation<Sndr, Rcvr>opbe an lvalue subexpression of that type. Thendenotes a specialization ofcompletion-signatures-for<Sndr, Env>completion_signatures, the set of whose template arguments corresponds to the set of completion operations that are potentially evaluated ([basic.def.odr]) as a result of evaluatingop.start(). Otherwise,is ill-formed. Ifcompletion-signatures-for<Sndr, Env>is well-formed and its type is not dependent upon the typecompletion-signatures-for<Sndr, Env>Env,is well-formed and denotes the same type; otherwise,completion-signatures-for<Sndr>is ill-formed.completion-signatures-for<Sndr>template <class Sndr, class... Env> static constexpr voiddefault-impls::check-types();? Let
Isbe the pack of integral template arguments of theinteger_sequencespecialization denoted by.indices-for<Sndr>? Effects: Equivalent to:
(get_completion_signatures<child-type<Sndr, Is>,FWD-ENV-T(Env)...>(), ...)? Remarks: For any types
T,S, and packE, letebe the expression. Then exactly one of the following isimpls-for<T>::check-types<S, E...>()true:
(?.1)
eis ill-formed, or(?.3) The evaluation of
eexits with an exception, or(?.2)
eis a core constant expression.When
eis a core constant expression, the typesS,E...uniquely determine a set of completion signatures.template<class Tag, class Data, class... Child> template <class Sndr, class... Env> constexpr autobasic-sender<Tag, Data, Child...>::get_completion_signatures();? Let
Rcvrbe the type of a receiver whose environment has typeE, whereEis the first type in the listEnv..., env<>. Letbe the expressionCHECK-TYPES(), and letimpls-for<Tag>::templatecheck-types<Sndr, E>()CSbe a type determined as follows:
(?.1) If
is a core constant expression, letCHECK-TYPES()opbe an lvalue subexpression whose type isconnect_result_t<Sndr, Rcvr>. ThenCSis the specialization ofcompletion_signaturesthe set of whose template arguments correspond to the set of completion operations that are potentially evaluated ([basic.def.odr]) as a result of evaluatingop.start().(?.2) Otherwise,
CSiscompletion_signatures<>.? Constraints:
is a well-formed expression.CHECK-TYPES()? Effects: Equivalent to
CHECK-TYPES(); return CS();
[ Editor's note: Change
the specification of
write-env in
[exec.snd.expos] para 40-43 as follows: ]
template<sender Sndr, queryable Env> constexpr autowrite-env(Sndr&& sndr, Env&& env); // exposition only40
write-envis an exposition-only sender adaptor that, when connected with a receiverrcvr, connects the adapted sender with a receiver whose execution environment is the result of joining the queryable argumentenvto the result ofget_env(rcvr).41 Let
write-env-tbe an exposition-only empty class type.42 Returns:
make-sender(write-env-t(), std::forward<Env>(env), std::forward<Sndr>(sndr))43 Remarks: The exposition-only class template
impls-for([exec.snd.general]) is specialized forwrite-env-tas follows:template<> structimpls-for<write-env-t> :default-impls{static constexpr autojoin-env(const auto& state, const auto& env) noexcept {returnsee below;}static constexpr autoget-env= [](auto, const auto& state, const auto& rcvr) noexcept { returnsee below; };join-env(state,FWD-ENV(get_env(rcvr)))template<class Sndr, class... Env>static constexpr void};check-types();Invocation of
returns an objectimpls-for<write-env-t>::get-envjoin-envesuch that
(43.1)
decltype(e)modelsqueryableand(43.2) given a query object
q, the expressione.query(q)is expression-equivalent tostate.query(q)if that expression is valid, otherwise,e.query(q)is expression-equivalent to.get_env(rcvr)env.query(q)
- (43.3) For type
Sndrand packEnv, letStatebeand letdata-type<Sndr>JoinEnvbe the packdecltype(. Thenjoin-env(declval<State>(),FWD-ENV(declval<Env>())))is expression-equivalent toimpls-for<write-env-t>::check-types<Sndr, Env...>()get_completion_signatures<.child-type<Sndr>, JoinEnv...>()
[ Editor's note: Add the following new paragraphs to the end of [exec.snd.expos] ]
?template<class... Fns> structoverload-set: Fns... { using Fns::operator()...; };[ Editor's note: The following is moved from [exec.on] para 6 and modified. ]
??structnot-a-sender{ using sender_concept = sender_t; template<class Sndr> static constexpr auto get_completion_signatures() -> completion_signatures<> { throw unspecified; } };constexpr voiddecay-copyable-result-datums(auto cs){ cs.for-each([]<class Tag, class... Ts>(Tag(*)(Ts...)) { if constexpr (!(is_constructible_v<decay_t<Ts>, Ts> &&...)) throwunspecified; }); }
[ Editor's note: Change [exec.snd.concepts] para 1 and add a new para after 1 as follows: ]
1 The
senderconcept … as before … to produce an operation state.namespace std::execution {template<class Sigs>conceptvalid-completion-signatures=see below; // exposition onlytemplate<auto>conceptis-constant= true;// exposition onlytemplate<class Sndr> conceptis-sender= // exposition only derived_from<typename Sndr::sender_concept, sender_t>; template<class Sndr> conceptenable-sender= // exposition onlyis-sender<Sndr> ||is-awaitable<Sndr,env-promise<env<>>>; // [exec.awaitable]template<class Sndr>consteval boolis-dependent-sender-helper() try {// exposition onlyget_completion_signatures<Sndr>();return false;} catch (dependent-sender-error&) {return true;}template<class Sndr> concept sender = bool(enable-sender<remove_cvref_t<Sndr>>) && requires (const remove_cvref_t<Sndr>& sndr) { { get_env(sndr) } ->queryable; } && move_constructible<remove_cvref_t<Sndr>> && constructible_from<remove_cvref_t<Sndr>, Sndr>; template<class Sndr, class... Env> concept sender_in = sender<Sndr> && (queryable<Env> &&...) &&is-constant<get_completion_signatures<Sndr, Env...>()>requires (Sndr&& sndr, Env&&... env) {{ get_completion_signatures(std::forward<Sndr>(sndr), std::forward<Env>(env)...) }->valid-completion-signatures;};template<class Sndr>concept dependent_sender =sender<Sndr> && bool_constant<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)); }; }is-dependent-sender-helper<Sndr>()>::value;
- ? For a type
Sndr, ifsender<Sndr>istrueanddependent_sender<Sndr>isfalse, thenSndris a non-dependent sender ([exec.async.ops]).
[ Editor's note: Strike [exec.snd.concepts] para 3 (this para is moved to [execution.syn]): ]
3
A type models the exposition-only conceptvalid-completion-signaturesif it denotes a specialization of thecompletion_signaturesclass template.
[ Editor's note: Change [exec.getcomplsigs] as follows: ]
1
get_completion_signaturesis a customization point object. Letsndrbe an expression such thatdecltype((sndr))isSndr, and letenvbe a pack of zero or one expression. Ifsizeof...(env) == 0istrue, letnew_sndrbesndr; otherwise, letnew_sndrbe the expressiontransform_sender(decltype(. Letget-domain-late(sndr, env...)){}, sndr, env...)NewSndrbedecltype((new_sndr)). Thenget_completion_signatures(sndr, env...)is expression-equivalent to(void(sndr), void(env)..., CS())except thatvoid(sndr)andvoid(env)...are indeterminately sequenced, whereCSis:
(1.1)
decltype(new_sndr.get_completion_signatures(env...))if that type is well-formed,(1.2) Otherwise, if
sizeof...(env) == 1istrue, thendecltype(new_sndr.get_completion_signatures())if that expression is well-formed,(1.3) Otherwise,
remove_cvref_t<NewSndr>::completion_signaturesif that type is well-formed,(1.4) Otherwise, if
isis-awaitable<NewSndr,env-promise<decltype((env))>...>true, then:completion_signatures<SET-VALUE-SIG(await-result-type<NewSndr, env-promise<Env>>), // ([exec.snd.concepts]) set_error_t(exception_ptr), set_stopped_t()>(1.4) Otherwise,
CSis ill-formed.
template <class Sndr, class... Env> consteval auto get_completion_signatures() ->;valid-completion-signaturesauto? Let
exceptbe an rvalue subexpression of an unspecified class typeExceptsuch thatmove_constructible<Except> && derived_from<Except, exception>istrue. LetbeCHECKED-COMPLSIGS(e)eifeis a core constant expression whose type satisfiesvalid-completion-signatures; otherwise, it is the following expression:(e, throwexcept, completion_signatures())Let
be expression-equivalent toget-complsigs<Sndr, Env...>()remove_reference_t<Sndr>::template get_completion_signatures<Sndr, Env...>(), letbe an alias for the typenested-complsigs-t<Sndr>typename remove_reference_t<Sndr>::completion_signatures, and letNewSndrbeSndrifsizeof...(Env) == 0istrue; otherwise,decltype(wheres)sis the following expression:transform_sender(get-domain-late(declval<Sndr>(), declval<Env>()...), declval<Sndr>(), declval<Env>()...)? Constraints:
sizeof...(Env) <= 1istrue.? Effects: Equivalent to:
return e;whereeis expression-equivalent to the following:
(?.1)
ifCHECKED-COMPLSIGS(get-complsigs<NewSndr, Env...>())is a well-formed expression.get-complsigs<NewSndr, Env...>()(?.2) Otherwise,
ifCHECKED-COMPLSIGS(get-complsigs<NewSndr>())is a well-formed expression.get-complsigs<NewSndr>()(?.3) Otherwise,
ifCHECKED-COMPLSIGS(nested-complsigs-t<NewSndr>())is a well-formed expression.nested-complsigs-t<NewSndr>()(?.4) Otherwise,
completion_signatures<SET-VALUE-SIG(await-result-type<NewSndr,env-promise<Env>...>), // ([exec.snd.concepts]) set_error_t(exception_ptr), set_stopped_t()>if
isis-awaitable<NewSndr,env-promise<Env>...>true.(?.5) Otherwise,
(throwifdependent-sender-error(), completion_signatures())sizeof...(Env) == 0istrue,(?.6) Otherwise,
(throw.except, completion_signatures())2 [ Editor's note: This para is no longer needed because the new
dependent_senderconcept covers it. ]Ifget_completion_signatures(sndr)is well-formed and its type denotes a specialization of thecompletion_signaturesclass template, thenSndris a non-dependent sender type ([exec.async.ops]).3 Given a type
Env, ifcompletion_signatures_of_t<Sndr>andcompletion_signatures_of_t<Sndr, Env>are both well-formed, they shall denote the same type.4 Let
rcvrbe an rvalue whose typeRcvrmodelsreceiver, and letSndrbe the type of a sender such thatsender_in<Sndr, env_of_t<Rcvr>>istrue. LetSigs...be the template arguments of thecompletion_signaturesspecialization named bycompletion_signatures_of_t<Sndr, env_of_t<Rcvr>>. LetCSObe a completion function. If senderSndror its operation state cause the expressionCSO(rcvr, args...)to be potentially evaluated ([basic.def.odr]) then there shall be a signatureSiginSigs...such thatMATCHING-SIG(decayed-typeof<CSO>(decltype(args)...), Sig)is
true([exec.general]).
[ Editor's note: At the very bottom of [exec.connect], change the Mandates of para 6 as follows: ]
6 The expression
connect(sndr, rcvr)is expression-equivalent to:
(6.1)
new_sndr.connect(rcvr)if that expression is well-formed.Mandates: The type of the expression above satisfies
operation_state.(6.2) Otherwise,
.connect-awaitable(new_sndr, rcvr)Mandates:
The following aresender<Sndr> && receiver<Rcvr>true:
[ Editor's note: In [exec.read.env] para 3, make the following change: ]
3 The exposition-only class template
impls-for([exec.snd.general]) is specialized forread_envas follows:namespace std::execution { template<> structimpls-for<decayed-typeof<read_env>> :default-impls{ static constexpr autostart= [](auto query, auto& rcvr) noexcept -> void {TRY-SET-VALUE(std::move(rcvr), query(get_env(rcvr))); };template<class Sndr, class Env>static constexpr void}; }check-types();
template<class Sndr, class Env> static constexpr voidcheck-types();
[ Editor's note: Change [exec.schedule.from] para 4 and insert a new para between 6 and 7 as follows: ]
4 The exposition-only class template
impls-for([exec.snd.general]) is specialized forschedule_from_tas follows:namespace std::execution { template<> structimpls-for<schedule_from_t> :default-impls{ static constexpr autoget-attrs=see below;static constexpr autoget-state=see below;static constexpr autocomplete=see below;template<class Sndr, class... Env>static constexpr void}; }check-types();5 The member … as before …
6 The member
is initialized with a callable object equivalent to the following lambda: [ Editor's note: This integrates the resolution from LWG#4203. ]impls-for<schedule_from_t>::get-state[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(see below) requires sender_in<child-type<Sndr>,env_of_t<Rcvr>FWD-ENV-T()> { auto& [_, sch, child] = sndr; … as before …template<class Sndr, class... Env> static constexpr voidcheck-types();? Effects: Equivalent to:
get_completion_signatures<schedule_result_t<data-type<Sndr>>,FWD-ENV-T(Env)...>(); auto cs = get_completion_signatures<child-type<Sndr>,FWD-ENV-T(Env)...>();decay-copyable-result-datums(cs); // see [exec.snd.expos]7 Objects of the local class
state-type… as before …8 Let
Sigsbe a pack of the arguments to thecompletion_signaturesspecialization named bycompletion_signatures_of_t<. Letchild-type<Sndr>,FWD-ENV-T(env_of_t<Rcvr>)>as-tuplebe an alias template such thatdenotes the typeas-tuple<Tag(Args...)>. Thendecayed-tuple<Tag, Args...>variant_tdenotes the typevariant<monostate,, except with duplicate types removed.as-tuple<Sigs>...>
[ Editor's note: Change
[exec.on] para 6 as follows
(not-a-sender is moved
to [exec.snd.expos]): ]
6 Otherwise: Let
not-a-schedulerbe an unspecified empty class type., and letnot-a-senderbe the exposition-only type:structnot-a-sender{ using sender_concept = sender_t; auto get_completion_signatures(auto&&) const { return see below; } };
where the member functionget_completion_signaturesreturns an object of a type that is not a specialization of thecompletion_signaturesclass template.
[ Editor's note: Delete [exec.on] para 9 as follows: ]
9
Recommended practice: Implementations should use the return type ofto inform users that their usage of on is incorrect because there is no available scheduler onto which to restore execution.not-a-sender::get_completion_signatures
[ Editor's note: Revert the change to [exec.then] made by P3164R3, and then change [exec.then] para 4 as follows: ]
4 The exposition-only class template
impls-for([exec.snd.general]) is specialized forthen-cpoas follows:namespace std::execution { template<> structimpls-for<decayed-typeof<then-cpo>> :default-impls{ static constexpr auto complete = []<class Tag, class... Args> (auto, auto& fn, auto& rcvr, Tag, Args&&... args) noexcept -> void { if constexpr (same_as<Tag,decayed-typeof<set-cpo>>) {TRY-SET-VALUE(rcvr, invoke(std::move(fn), std::forward<Args>(args)...)); } else { Tag()(std::move(rcvr), std::forward<Args>(args)...); } };template<class Sndr, class... Env>static constexpr void}; }check-types();
?template<class Sndr, class... Env> static constexpr voidcheck-types();
(?.1) Effects: Equivalent to:
auto cs = get_completion_signatures<child-type<Sndr>,FWD-ENV-T(Env)...>(); auto fn = []<class... Ts>(set_value_t(*)(Ts...)) { if constexpr (!invocable<remove_cvref_t<data-type<Sndr>>, Ts...>) throwunspecified; }; cs.for-each(overload-set{fn, [](auto){}});
[ Editor's note: Revert the change to [exec.let] made by P3164R3, and then change [exec.let] para 5 and insert a new para after 5 as follows: ]
5 The exposition-only class template
impls-for([exec.snd.general]) is specialized forlet-cpoas follows:namespace std::execution { template<class State, class Rcvr, class... Args> voidlet-bind(State& state, Rcvr& rcvr, Args&&... args); // exposition only template<> structimpls-for<decayed-typeof<let-cpo>> :default-impls{ static constexpr autoget-state=see below; static constexpr autocomplete=see below;template<class Sndr, class... Env>static constexpr void}; }check-types();6 Let
receiver2denote the following exposition-only class template:namespace std::execution { … as before … }Invocation of the function
returns an objectreceiver2::get_envesuch that
(6.1)
decltype(e)modelsqueryableand(6.2) given a query object
q, the expressione.query(q)is expression-equivalent toif that expression is validenv.query(q),; otherwise, if the type ofqsatisfiesforwarding-query,e.query(q)is expression-equivalent toget_env(; otherwise,rcvr).query(q)e.query(q)is ill-formed.?template<class Sndr, class... Env> static constexpr voidcheck-types();
(?.1) Effects: Equivalent to:
using LetFn = remove_cvref_t<data-type<Sndr>>; auto cs = get_completion_signatures<child-type<Sndr>,FWD-ENV-T(Env)...>(); auto fn = []<class... Ts>(decayed-typeof<set-cpo>(*)(Ts...)) { if constexpr (!is-valid-let-sender) throwunspecified; }; cs.for-each(overload-set(fn, [](auto){}));where
is-valid-let-senderistrueif and only if all of the following aretrue:
- (?.1.1)
(constructible_from<decay_t<Ts>, Ts> &&...)- (?.1.2)
invocable<LetFn, decay_t<Ts>&...>- (?.1.3)
sender<invoke_result_t<LetFn, decay_t<Ts>&...>>- (?.1.4)
sizeof...(Env) == 0 || sender_in<invoke_result_t<LetFn, decay_t<Ts>&...>,env-t...>where
env-tis the packdecltype(.let-cpo.transform_env(declval<Sndr>(), declval<Env>()))[ Editor's note: The following changes are the proposed resolutions to cplusplus/sender-receiver#316 and cplusplus/sender-receiver#318. ]
7
is initialized with a callable object equivalent to the following:impls-for<decayed-typeof<let-cpo>>::get-state[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) requiressee below{ auto& [_, fn, child] = sndr; using fn_t = decay_t<decltype(fn)>; using env_t = decltype(let-env(child)); using args_variant_t =see below; using ops2_variant_t =see below; … as before … }
(7.1) Let
Sigsbe a pack of the arguments to thecompletion_signaturesspecialization named bycompletion_signatures_of_t<. Letchild-type<Sndr>,FWD-ENV-T(env_of_t<Rcvr>)>LetSigsbe a pack of those types inSigswith a return type of. Letdecayed-typeof<set-cpo>as-tuplebe an alias template such thatdenotes the typeas-tuple<Tag(Args...)>. Thendecayed-tuple<Args...>args_variant_tdenotes the typevariant<monostate,except with duplicate types removed.as-tuple<LetSigs>...>(7.2) Given a type
Tagand a packArgs, letas-sndr2be an alias template such thatdenotes the typeas-sndr2<Tag(Args...)>. Thencall-result-t<Fn, decay_t<Args>&...>ops2_variant_tdenotes the typevariant<monostate, connect_result_t<as-sndr2<LetSigs>, receiver2<Rcvr,Envenv_t>>...>except with duplicate types removed.
(7.3) The
requires-clauseconstraining the above lambda is satisfied if and only if the typesargs_variant_tandops2_variant_tare well-formed.11 The exposition-only function template
let-bindhas effects equivalent to: … as before …12 … as before …
[ Editor's note: The following change to [exec.let] para 13 is the proposed resolution to cplusplus/sender-receiver#319. ]
13 Let
sndrandenvbe subexpressions, and letSndrbedecltype((sndr)). Ifissender-for<Sndr,decayed-typeof<let-cpo>>false, then the expressionis ill-formed. Otherwise, it is equal tolet-cpo.transform_env(sndr, env).JOIN-ENV(let-env(sndr),FWD-ENV(env))auto& [_, _, child] = sndr;returnJOIN-ENV(let-env(child),FWD-ENV(env));
[ Editor's note: Revert the change to [exec.bulk] made by P3164R3, and then change [exec.bulk] para 3 and insert a new para after 5 as follows: ]
3 The exposition-only class template
impls-for([exec.snd.general]) is specialized forbulk_tas follows:namespace std::execution { template<> structimpls-for<bulk_t> :default-impls{ static constexpr autocomplete=see below;template<class Sndr, class... Env>static constexpr void}; }check-types();4 The member
is … as before …impls-for<bulk_t>::complete5 … as before …
?template<class Sndr, class... Env> static constexpr voidcheck-types();
(?.1) Effects: Equivalent to:
auto cs = get_completion_signatures<child-type<Sndr>,FWD-ENV-T(Env)...>(); auto fn = []<class... Ts>(set_value_t(*)(Ts...)) { if constexpr (!invocable<remove_cvref_t<data-type<Sndr>>, Ts&...>) throwunspecified; }; cs.for-each(overload-set{fn, [](auto){}});
[ Editor's note: Revert the change to [exec.split] made by P3164R3, and then change [exec.split] para 3 and insert a new para after 3 as follows: ]
3 The name
splitdenotes a pipeable sender adaptor object.For a subexpressionsndr, letSndrbedecltype((sndr)). Ifsender_in<Sndr,issplit-env>false,split(sndr)is ill-formed.
? The exposition-only class template
impls-for([exec.snd.general]) is specialized forsplit_tas follows:namespace std::execution { template<> structimpls-for<split_t> :default-impls{ template<class Sndr> static constexpr voidcheck-types() { auto cs = get_completion_signatures<child-type<Sndr>,split-env>();decay-copyable-result-datums(cs); // see [exec.snd.expos] } }; }
[ Editor's note: Change [exec.when.all] paras 2-9 and insert two new paras after 4 as follows: ]
2 The names
when_allandwhen_all_with_variantdenote customization point objects. Letsndrsbe a pack of subexpressions, letSndrsbe a pack of the typesdecltype((sndrs))..., and letCDbe the typecommon_type_t<decltype(, and letget-domain-early(sndrs))...>CD2beCDifCDis well-formed, anddefault_domainotherwise.. The expressionswhen_all(sndrs...)andwhen_all_with_variant(sndrs...)are ill-formed if any of the following is true:
- (2.3)
CDis ill-formed.3 The expression
when_all(sndrs...)is expression-equivalent to:transform_sender(CD()CD2(),make-sender(when_all, {}, sndrs...))4 The exposition-only class template
impls-for([exec.snd.general]) is specialized forwhen_all_tas follows:namespace std::execution { template<> structimpls-for<when_all_t> :default-impls{ static constexpr autoget-attrs=see below; static constexpr autoget-env=see below; static constexpr autoget-state=see below; static constexpr autostart=see below; static constexpr autocomplete=see below;template<class Sndr, class... Env>static constexpr void}; }check-types();? Let
make-when-all-envbe the following exposition-only function template:template<class Env> constexpr automake-when-all-env(inplace_stop_source& stop_src, Env&& env) noexcept { returnsee below; }Returns an object
esuch that The following itemized list has been moved here from para 6 and modified.
(?.1)
decltype(e)modelsqueryable, and(?.2)
e.query(get_stop_token)is expression-equivalent tostop_src.get_token(), and(?.3) given a query object
qwith type other than cvstop_token_tand whose type satisfiesforwarding-query,e.query(q)is expression-equivalent toenv.query(q).Let
when-all-envbe an alias template such thatdenotes the typewhen-all-env<Env>decltype(.make-when-all-env(declval<inplace_stop_source&>(), declval<Env>()))
?template<class Sndr, class... Env> static constexpr voidcheck-types();
(?.1) Let
Isbe the pack of integral template arguments of theinteger_sequencespecialization denoted by.indices-for<Sndr>(?.2) Effects: Equivalent to:
auto fn = []<class Child>() { auto cs = get_completion_signatures<Child,when-all-env<Env>...>(); if constexpr (cs.count-of(set_value) >= 2) throwunspecified;decay-copyable-result-datums(cs); // see [exec.snd.expos] }; (fn.template operator()<child-type<Sndr, Is>>(), ...);(?.3) Throws: Any exception thrown as a result of evaluating the Effects, or an exception of an unspecified type when
CDis ill-formed.
5 The member
… as before …impls-for<when_all_t>::get-attrs6 The member
is initialized with a callable object equivalent to the following lambda expression:impls-for<when_all_t>::get-env[]<class State, class Rcvr>(auto&&, State& state, const Receiver& rcvr) noexcept { returnsee below; }make-when-all-env(state.stop-src, get_env(rcvr))
Returns an objectesuch that
7 The member
is initialized with a callable object equivalent to the following lambda expression:impls-for<when_all_t>::get-state[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(e) -> decltype(e) { return e; }where
eis the expressionstd::forward<Sndr>(sndr).apply(make-state<Rcvr>())and where
make-stateis the following exposition-only class template:template<class Sndr, class Env>conceptmax-1-sender-in= sender_in<Sndr, Env> &&@// exposition onlyenum class disposition { started, error, stopped }; // exposition only template<class Rcvr> struct make-state { template<(tuple_size_v<value_types_of_t<Sndr, Env, tuple, tuple>> <= 1);class... Sndrs> auto operator()(auto, auto, Sndrs&&... sndrs) const { using values_tuple = see below; using errors_variant = see below; using stop_callback = stop_callback_for_t<stop_token_of_t<env_of_t<Rcvr>>, on-stop-request>; … as before …max-1-sender-in<env_of_t<Rcvr>>8 Let
copy-failbe … as before …9 The alias
values_tupledenotes the typetuple<value_types_of_t<Sndrs,env_of_t<Rcvr>FWD-ENV-T(), decayed-tuple, optional>...>if that type is well-formed; otherwise,
tuple<>.
[ Editor's note: Change [exec.when.all] para 14 as follows: ]
14 The expression
when_all_with_variant(sndrs...)is expression-equivalent to:transform_sender(CD()CD2(),make-sender(when_all_with_variant, {}, sndrs...));
[ Editor's note: Change [exec.into.variant] paras 4-5 as follows (with the change to para 5 being a drive-by fix): ]
4 The exposition-only class template
impls-for([exec.snd.general]) is specialized forinto_variantas follows:namespace std::execution { template<> structimpls-for<into_variant_t> :default-impls{ static constexpr autoget-state=see below; static constexpr autocomplete=see below;template<class Sndr, class... Env>static constexpr voidcheck-types() {auto cs = get_completion_signatures<child-type<Sndr>,FWD-ENV-T(Env)...>();// see [exec.snd.expos]decay-copyable-result-datums(cs);}}; }5 The member
is initialized with a callable object equivalent to the following lambda:impls-for<into_variant_t>::get-state[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept -> type_identity<value_types_of_t<child-type<Sndr>,env_of_t<Rcvr>FWD-ENV-T()>> { return {}; }
[ Editor's note: Revert the change to [exec.stopped.opt] made by P3164R3, and make the following changes instead. Note: this includes the proposed resolution to cplusplus/sender-receiver#311 ]
2 The name
stopped_as_optionaldenotes a pipeable sender adaptor object. For a subexpressionsndr, letSndrbedecltype((sndr)). The expressionstopped_as_optional(sndr)is expression-equivalent to:transform_sender(get-domain-early(sndr),make-sender(stopped_as_optional, {}, sndr))except that
sndris only evaluated once.? The exposition-only class template
impls-for([exec.snd.general]) is specialized forstopped_as_optional_tas follows:template<> structimpls-for<stopped_as_optional_t> :default-impls{ template<class Sndr, class... Env> static constexpr voidcheck-types() {default-impls::check-types<Sndr, Env...>(); if constexpr (!requires { requires (!same_as<void,single-sender-value-type<child-type<Sndr>,FWD-ENV-T(Env)...>>); }) throw unspecified; } };3 Let
sndrandenvbe subexpressions such thatSndrisdecltype((sndr))andEnvisdecltype((env)). Ifissender-for<Sndr, stopped_as_optional_t>false, or if the typethen the expressionis ill-formed orsingle-sender-value-type<Sndr, Env>void,stopped_as_optional.transform_sender(sndr, env)is ill-formed; otherwise, ifsender_in<ischild-type<Sndr>,FWD-ENV-T(Env)>false, the expressionstopped_as_optional.transform_sender(sndr, env)is equivalent to; otherwise, it is equivalent to:not-a-sender()auto&& [_, _, child] = sndr; using V =single-sender-value-type<Sndrchild-type<>,EnvFWD-ENV-T()>; return let_stopped( then(std::forward_like<Sndr>(child), []<class... Ts>(Ts&&... ts) noexcept(is_nothrow_constructible_v<V, Ts...>) { return optional<V>(in_place, std::forward<Ts>(ts)...); }), []() noexcept { return just(optional<V>()); });
[ Editor's note: Change [exec.util.cmplsig] para 8 and add a new para after 8 as follows: ]
8namespace std::execution { template<completion-signature... Fns> struct completion_signatures {template<class Tag>static constexpr size_tcount-of(Tag) { returnsee below; }template<class Fn>static constexpr voidfor-each(Fn&& fn) { // exposition only(std::forward<Fn>(fn)(static_cast<Fns*>(nullptr)), ...);}}; … as before … }
? For a subexpression
tag, letTagbe the decayed type oftag.completion_signatures<Fns...>::returns the count of function types incount-of(tag)Fns...that are of the formTag(Ts...)whereTsis a pack of types.
[ Editor's note: Remove subclause [exec.util.cmplsig.trans]. ]
[ Editor's note: Change [execution.syn] as follows (changes relative to what is already suggested above): ]
… as before … namespace std::execution { … as before … // [exec.util], sender and receiver utilities // [exec.util.cmplsig] completion signatures template<class Fn> conceptcompletion-signature=see below; // exposition only template<completion-signature... Fns> struct completion_signatures; template<class Sigs> conceptvalid-completion-signatures=see below; // exposition only structdependent-sender-error: exception {}; // exposition only // [exec.getcomplsigs] template<class Sndr, class... Env> consteval auto get_completion_signatures() ->valid-completion-signaturesauto; template<class Sndr, class... Env> requires sender_in<Sndr, Env...> using completion_signatures_of_t = decltype(get_completion_signatures<Sndr, Env...>());template<bool PotentiallyThrowing>inline constexpr auto eptr_completion_if = completion_signatures();template<>inline constexpr auto eptr_completion_if<true> =completion_signatures<set_error_t(exception_ptr)>();// [exec.util.cmplsig.getchild]template <class Parent, class Child, class... Env>consteval auto get_child_completion_signatures() ->valid-completion-signaturesauto;// [exec.util.cmplsig.make]template <completion-signature... ExplicitSigs,completion-signature... DeducedSigs>constexpr auto make_completion_signatures(DeducedSigs*... sigs) noexcept->valid-completion-signaturesauto;// [exec.util.cmplsig.invalid]template <class... Types, class... Values>[[noreturn, nodiscard]]consteval auto invalid_completion_signature(Values... values)-> completion_signatures<>;// [exec.util.cmplsig.trans]template<valid-completion-signaturesCompletions,class ValueTransform =see below,class ErrorTransform =see below,class StoppedTransform =see below,valid-completion-signaturesExtraCompletions =see below>constexpr auto transform_completion_signatures(Completions completions,ValueTransform value_transform = {},ErrorTransform error_transform = {},StoppedTransform stopped_transform = {},ExtraCompletions extra_completions = {});// [exec.run.loop], run_loop class run_loop; } … as before …
[ Editor's note: Make the completion tags equality comparable with each other. Insert a new paragraph after [exec.rcvr.concepts] as follows: ]
(33.7.?) Completion tags [exec.set.tag]
1 The types
set_value_t,set_error_t, andset_stopped_tare completion tag types ([exec.async.ops]). Each modelsequality_comparablewith instances comparing equal to each other. Each also modelsequality_comparable_withthe other two, with objects of different types comparing not equal. For two completion tag objectsaandb,a == banda != bare core constant expressions and are not potentially throwing. [ Example:set_value_t() == set_value_t()is a constant expression that evaluates totrue, andset_value_t() == set_error_t()is a constant expression that evaluates tofalse. — end example ]
[ Editor's note: Give the
completion_signatures class
template some constexpr members
for manipulating its instances. Instead of the change suggested above to
[exec.util.cmplsig] para 8, split para 8 into two after
struct completion_signatures and
modify as follows: ]
8namespace std::execution { template<completion-signature... Fns> struct completion_signatures {completion_signatures() = default;constexpr explicit completion_signatures(Fns*...) noexceptrequires (0 != sizeof...(Fns)) {}static constexpr size_t size() noexcept { return sizeof...(Fns); }template<class... Others>static constexpr bool contains(completion_signatures<Others...>) noexcept;template<class Fn>static constexpr auto filter(Fn) noexcept;template<class Tag>static constexpr auto select(Tag) noexcept;template<class MapFn,callable<call-result-t<MapFn, Fns*>...> ReduceFn>static constexpr auto transform_reduce(MapFn map, ReduceFn reduce);template<class... Others>constexpr auto operator+(completion_signatures<Others...>) noexcept;template<class... Others>constexpr bool operator==(completion_signatures<Others...> other) noexcept;};template<size_t I, class... Fns>constexpr auto get(completion_signatures<Fns...>) noexcept -> Fns...[I]* {return nullptr;}}namespace std {using execution::get;template<class... Fns>struct tuple_size<execution::completion_signatures<Fns...>>: integral_constant<size_t, sizeof...(Fns)> {};template<size_t I, class... Fns>struct tuple_element<I, execution::completion_signatures<Fns...>> {using type = Fns...[I]*;};}
- (8.1) Mandates: For the specialization
completion_signatures<Fns...>, the typesare unique.NORMALIZE-SIG(Fns)...template<class... Others> constexpr bool contains(completion_signatures<Others...>) noexcept;
- (8.2) Returns:
trueif and only if for all typesTinOthers...the expression(isMATCHING-SIG(T, Fns) ||...)true.template<class Fn> constexpr auto filter(Fn) noexcept;
(8.3) For a type
Retand packArgs, letdenote the typemaybe-completion<Ret(Args...)>completion_signatures<Ret(Args...)>ifiscallable<Fn, Ret(*)(Args...)>true, andcompletion_signatures<>otherwise.(8.4) Returns:
(completion_signatures() +...+.maybe-completion<Fns>())template<class Tag> constexpr auto select(Tag) noexcept;
- (8.5) Returns:
filter([]<class... Ts>(Tag(*)(Ts...)) {}).template<class MapFn,callable<call-result-t<MapFn, Fns*>...> ReduceFn> constexpr auto transform_reduce(MapFn map, ReduceFn reduce);
- (8.6) Returns:
reduce(map(static_cast<Fns*>(nullptr))...).template<class... Others> constexpr auto operator+(completion_signatures<Others...>) noexcept;
- (8.7) Returns:
completion_signatures<Us...>(), whereUsis the pack of the types inandNORMALIZE-SIG(Fns)...with duplicate types removed.NORMALIZE-SIG(Others)...template<class... Others> constexpr bool operator==(completion_signatures<Others...> other) const noexcept;
- (8.7) Returns:
size() == other.size() && contains(other).[ Editor's note: The following is split from what is currently para 8 in the Working Draft. ]
9namespace std::execution { template<class Sndr, class Env = env<>, template<class...> class Tuple =decayed-tuple, template<class...> class Variant =variant-or-empty> requires sender_in<Sndr, Env> using value_types_of_t =gather-signatures<set_value_t, completion_signatures_of_t<Sndr, Env>, Tuple, Variant>; template<class Sndr, class Env = env<>, template<class...> class Variant =variant-or-empty> requires sender_in<Sndr, Env> using error_types_of_t =gather-signatures<set_error_t, completion_signatures_of_t<Sndr, Env>, type_identity_t, Variant>; template<class Sndr, class Env = env<>> requires sender_in<Sndr, Env> constexpr bool sends_stopped = !same_as<type-list<>,gather-signatures<set_stopped_t, completion_signatures_of_t<Sndr, Env>,type-list,type-list>>; }
[ Editor's note: After [exec.util.cmplsig], insert a three new sections as follows: ]
(33.10.?)
execution::get_child_completion_signatures[exec.util.cmplsig.getchild]template <class Parent, class Child, class... Env> consteval auto get_child_completion_signatures() ->;valid-completion-signaturesauto? Effects: Equivalent to:
using CvChild = decltype(std::forward_like<Parent>(declval<Child&>())); return get_completion_signatures<CvChild,FWD-ENV-T(Env)...>();(33.10.?)
execution::make_completion_signatures[exec.util.cmplsig.make]template <completion-signature... ExplicitSigs,completion-signature... DeducedSigs> constexpr auto make_completion_signatures(DeducedSigs*... sigs) noexcept ->valid-completion-signaturesauto;
- 1 Returns:
completion_signatures<Ts...>(), whereTsis a pack of the typeswith duplicate types removed.NORMALIZE-SIG(ExplicitSigs)...,NORMALIZE-SIG(DeducedSigs)...(33.10.?)
execution::invalid_completion_signature[exec.util.cmplsig.invalid]template <class... Types, class... Values> [[noreturn, nodiscard]] consteval auto invalid_completion_signature(Values... values) -> completion_signatures<>;
1
invalid_completion_signatureis used to report type errors encountered while computing a sender’s completion signatures.2 [Example 1: A
current_scheduleralgorithm might useinvalid_completion_signaturein its sender type as follows:template <const auto&> struct IN_ALGORITHM; extern const struct current_scheduler_t current_scheduler; // an algorithm template <class Sndr, class Env> constexpr auto current_scheduler_sender::get_completion_signatures() { if constexpr (invocable<get_scheduler_t, Env>) { using Result = invoke_result_t<get_scheduler_t, Env>; return completion_signatures<set_value_t(Result)>(); } else { // Oh no, the user made an error! Tell them about it. return invalid_completion_signature< IN_ALGORITHM<current_scheduler>, struct NO_SCHEDULER_IN_THE_CURRENT_ENVIRONMENT, struct WITH_ENVIRONMENT(Env) >("The current execution environment does not have a value " "for the get_scheduler query."); } }– end example]
3 Effects: Equivalent to
return (throw.unspecified, completion_signatures());4 Throws: An exception of an unspecified type derived from
exception.5 Recommended practice: Implementations are encouraged to throw an exception such that the template parameters and function arguments of
invalid_completion_signatureappear in the compiler diagnostic should the exception propagate out of aconstevalcontext.
[ Editor's note: Replace the section [exec.util.cmplsig.trans] in the Working Draft with the following: ]
(33.10.2)
execution::transform_completion_signatures[exec.util.cmplsig.trans]1
transform_completion_signaturesisan aliasa function template used to transform one set of completion signatures into another. It takes a set of completion signatures and several othertemplatearguments that apply modifications to each completion signature in the set to generate an instance of a new specialization ofcompletion_signatures.2 [Example 1: Given a sender type
Sndrand an environmentEnv, adapt the completion signatures ofSndrby lvalue-ref qualifying the values, adding an additionalexception_ptrerror completion if it is not already there, and leaving the other completion signatures alone.template<class... Args> using my_set_value_t = completion_signatures< set_value_t(add_lvalue_reference_t<Args>...)>;usingautomy_completion_signatures = transform_completion_signatures<(,get_completion_signatures<Sndr, Env>_of_t()[]<class... Args>() { return my_set_value_t<Args...>(); },{}, // no-op error signature transform{}, // no-op stopped signature transformcompletion_signatures<set_error_t(exception_ptr)>());,my_set_value_t>;– end example]
[ Editor's note: Replace the remaining paragraphs of [exec.util.cmplsig.trans] with the following: ]
3 This subclause makes use of the following exposition-only entities:
template<class Tag> constexpr autopass-thru-transform= []<class... Ts>() noexcept { return completion_signatures<Tag(Ts...)>(); }; constexpr autoconcat-completions= [](auto... cs) noexcept { return (completion_signatures() +...+ cs); };4
(3.1) For a subexpression
fnand a pack of typesArgs, letbe expression-equivalent toTRANSFORM-EXPR(fn, Args)fn()ifsizeof...(Args) == 0istrueandfn.template operator()<Args...>()otherwise. Letbe expression-equivalent to:APPLY-TRANSFORM(fn, Args)
(3.1.1)
if that expression is well-formed and has a type that satisfiesTRANSFORM-EXPR(fn, Args)valid-completion-signatures.(3.1.2) Otherwise,
(if that expression is well-formed.TRANSFORM-EXPR(fn, Args), throwunspecified, completion_signatures())(3.1.3) Otherwise,
(throw.unspecified, completion_signatures())template<valid-completion-signaturesCompletions, class ValueTransform = decltype(pass-thru-transform<set_value_t>), class ErrorTransform = decltype(pass-thru-transform<set_error_t>), class StoppedTransform = decltype(pass-thru-transform<set_stopped_t>),valid-completion-signaturesExtraCompletions = completion_signatures<>> constexpr auto transform_completion_signatures( Completions completions, ValueTransform value_transform = {}, ErrorTransform error_transform = {}, StoppedTransform stopped_transform = {}, ExtraCompletions extra_completions = {});
(4.1) Effects: Equivalent to
auto transform = [=]<class Tag, class... Args>(Tag(*)(Args...)) { if constexpr (same_as<Tag, set_value_t>) returnAPPLY-TRANSFORM(value_transform, Args); else if constexpr (same_as<Tag, set_error_t>) returnAPPLY-TRANSFORM(error_transform, Args); else returnAPPLY-TRANSFORM(stopped_transform, Args); }; return completions.transform_reduce(transform,concat-completions) + extra_completions;(4.2) Recommended practice: Users are encouraged to throw descriptive exceptions from their transformation functions when they encounter errors during type computation.
I would like to thank Hana Dusíková for her work making constexpr exceptions a reality for C++26. Thanks are also due to David Sankel for his encouragement to investigate using constexpr exceptions as an alternative to TMP hackery, and for giving feedback on an early draft of this paper.