This paper aims to improve the user experience of the sender framework by giving it better diagnostics when used incorrectly.
First, it moves the diagnosis of many invalid sender expression earlier, when the expression is constructed, rather than later when it is connected to a receiver. A trivial change to the sender adaptor algorithms makes it possible for the majority of sender expressions to be type-checked this way, giving the user immediate feedback when they've made a mistake.
Second, this paper proposed changes to the transform_completion_signatures
alias template to allow it to serve as a meta-exception propagation channel. This helps with late (connect
-time) type checking by allowing type computation errors from deeply nested senders to propagate to the API boundary, where they can be reported concisely.
Below are the specific changes this paper proposes in order to improve the diagnostics emitted by sender-based codes:
Define a "non-dependent sender" to be one whose completions are knowable without an environment.
Extend the awaitable helper concepts to support querying a type whether it is awaitable in an arbitrary coroutine (without knowing the promise type). For example, anything that implements the awaiter interface (await_ready
, await_suspend
, await_resume
) is awaitable in any coroutine, and should function as a non-dependent sender.
Add support for calling get_completion_signatures
without an environment argument.
Change the definition of the completion_signatures_of_t
alias template to support querying a sender's non-dependent signatures, if such exist.
Require the sender adaptor algorithms to preserve the "non-dependent sender" property wherever possible.
Add "Mandates:" paragraphs to the sender adaptor algorithms to require them to hard-error when passed non-dependent senders that fail type-checking.
Extend the eager type checking of the let_
family of algorithms to hard-error if the user passes a lambda that does not return a sender type.
Change transform_completion_signatures
to propagate any intermediate types that are not specializations of the completion_signatures<>
class template. For type errors that occur when computing a sender's completion signatures, sender authors can return a custom type that describes the error and have it automatically propagates through adaptors that use transform_completion_signatures
.
For any algorithm that eagerly connect
s a sender (sync_wait
, start_detached
, ensure_started
, split
), hard-error (i.e. static_assert
) if the sender fails to type-check rather than SFINAE-ing the overload away.
R1:
Change the specification of transform_completion_signatures
to propagate types that are not specialization of the completion_signatures<>
class template. This makes it easier to use an algorithm's completion signatures to communicate type errors from child senders.
For the customization points let_value
, let_error
, and let_stopped
, mandate that the callable's possible return types all satisfy sender
.
Change Requires: to Mandates: for algorithms that eagerly connect senders.
R0:
Type-checking a sender expression involves computing its completion signatures. In the general case, a sender's completion signatures may depend on the receiver's execution environment. For example, the sender:
read_env(get_stop_token)
... when connected to a receiver rcvr
and started, will fetch the stop token from the receiver's environment and then pass it back to the receiver, as follows:
auto st = get_stop_token(get_env(rcvr));
set_value(move(rcvr), move(st));
Without an execution environment, the sender read_env(get_stop_token)
doesn't know how it will complete.
The type of the environment is known rather late, when the sender is connected to a receiver. This is often far from where the sender expression was constructed. If there are type errors in a sender expression, those errors will be diagnosed far from where the error was made, which makes it harder to know the source of the problem.
It would be far preferable to issue diagnostics while constructing the sender rather than waiting until it is connected to a receiver.
The majority of senders have completions that don't depend on the receiver's environment. Consider just(42)
-- it will complete with the integer 42
no matter what receiver it is connected to. If a so-called "non-dependent" sender advertised itself as such, then sender algorithms could eagerly type-check the non-dependent senders they are passed, giving immediate feedback to the developer.
For example, this expression should be immediately rejected:
42) | then([](int* p) { return *p; }) just(
The then
algorithm can reject just(42)
and the above lambda because the arguments don't match: an integer cannot be passed to a function expecting an int*
. The then
algorithm can do that type-checking only when it knows the input sender is non-dependent. It couldn't, for example, do any type-checking if the input sender were read_env(get_stop_token)
instead of just(42)
.
And in fact, some senders do advertise themselves as non-dependent, although P2300 does not currently do anything with that extra information. A sender can declare its completions signatures with a nested type alias, as follows:
template <class T>
struct just_sender {
T value;
using completion_signatures =
std::execution::completion_signatures<
std::execution::set_value_t(T)
>;
// ...
};
Senders whose completions depend on the execution environment cannot declare their completion signatures this way. Instead, they must define a get_completion_signatures
customization that takes the environment as an argument.
We can use this extra bit of information to define a non_dependent_sender
concept as follows:
template <class Sndr>
concept non_dependent_sender =
sender<Sndr> &&requires {
typename remove_cvref_t<Sndr>::completion_signatures;
};
A sender algorithm can use this concept to conditionally dispatch to code that does eager type-checking.
The authors suggests that this notion of non-dependent senders be given fuller treatment in P2300. Conditionally defining the nested typedef in generic sender adaptors -- which may adapt either dependent or non-dependent senders -- is awkward and verbose. We suggest instead to support calling get_completion_signatures
either with or without an execution environment. This makes it easier for authors of sender adaptors to preserve the "non-dependent" property of the senders it wraps.
We suggest that a similar change be made to the completion_signatures_of_t
alias template. When instantiated with only a sender type, it should compute the non-dependent completion signatures, or be ill-formed.
Consider the following code, which contains a type error:
auto work = just(42)
int* p) { // <<< ERROR here
| then([](//...
});
The table below shows the result of compiling this code both before the proposed change and after:
Before |
After |
---|---|
no error |
error: static_assert failed due to requirement '_is_completion_signatures< ustdex::ERROR<ustdex::WHERE (ustdex::IN_ALGORITHM, ustdex::then_t), ustdex ::WHAT (ustdex::FUNCTION_IS_NOT_CALLABLE), ustdex::WITH_FUNCTION ((lambda at hello.cpp:57:18)), ustdex::WITH_ARGUMENTS (int)>>' static_assert(_is_completion_signatures<_completions>); ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
This error was generated with with µstdex library and Clang-13.
The addition of support for a customization of get_completion_signatures
that does not take an environment obviates the need to support the use of a nested ::completion_signatures
alias. In a class, this:
auto get_completion_signatures() ->
std::execution::completion_signatures<
std::execution::set_value_t(T)
>;
... works just as well as this:
using completion_signatures =
std::execution::completion_signatures<
std::execution::set_value_t(T)
>;
Without a doubt, we could simplify the design by dropping support for the latter. This paper suggests retaining it, though. For something like the just_sender
, providing type metadata with an alias is more idiomatic and less surprising, in the author's opinion, than defining a function and putting the metadata in the return type. That is the case for keeping the typename Sndr::completion_signatures
form.
The case for adding the sndr.get_completion_signatures()
form is that it makes it simpler for sender adaptors such as then_sender
to preserve the "non-dependent" property of the senders it adapts. For instance, one could define then_sender
like:
template <class Sndr, class Fun>
struct then_sender {
sndr_;
Sndr fun_;
Fun
template <class... Env>
auto get_completion_signatures(Env&&... env) const
-> some-computed-type;
//....
};
... and with this one member function support both dependent and non-dependent senders while preserving the "non-dependent-ness" of the adapted sender.
Experience implementing and using sender-based libraries has taught the author several things:
Concepts-based constraints on sender algorithms and their inner workings do more harm than good. The diagnostics are generally poor. The constraint failure may happen deep in a sender expression tree, but the diagnostic the user sees is simply: "no overload found". That gives users exactly zero information about the cause of the error.
Dropping the one-and-only possible overload from the overload set typically doesn't help users either, most of whom don't care about SFINAE. What they want are good diagnostics.
The current specification of the customization points and utilities make type errors SFINAE-able: either a construct type-checks or else it is ill-formed. That makes it very difficult for sender adaptors to propagate type errors from their child senders. In runtime code, we have exceptions to propagate errors to API boundaries. We have no equivalent for type computations, and P2300's current facilities offer no help.
To address the first issue, the author recommends using static_assert
s instead of requires
clauses for type errors in sender algorithms.
But what condition should we put in the static_assert
? If we use the same predicates that are in the requires
clauses, the errors will be little better. Instead of "no overload found", users will see: "static_assert: sender_to<Sndr, Rcvr> evaluated to false
", followed by a (lengthy and probably truncated) concepts backtrace. Buried in there somewhere may be the cause of the error for those entripid enough to dig for it.
This brings us to the second issue: propagating type errors from deep inside a sender tree evaluation to the API boundary where it can be concisely reported to the user.
The best way the author has found to report "late" (at connect
time) type-checking failures is via the sender's completion signatures. If a type error happens while trying to compute completion_signatures_of_t<Sndr, Env>
, instead of making the type ill-formed, it is better for it to name a type that communicates the error to the user.
Algorithms like sync_wait
can then static_assert
that the result of completion_signatures_of_t<Sndr, Env>
is a specialization of the completion_signatures
class template. If it instead names a type that is descriptive of the error, the name of that type will appear prominently in the compiler's (blissfully short) diagnostic.
Consider the following code, which has a type error in it:
// non-standard extension
thread_context ctx; auto sch = ctx.get_scheduler();
using namespace std::execution;
auto work = read_env(get_delegatee_scheduler)
auto sched) {
| let_value([](// create some work to delegate to the main thread.
auto delegated_work =
just() | then([] {std::puts("Hello, world!");
});
// launch the work on the delegation scheduler.
// <<< ERROR HERE
start_on(sched, delegated_work);
});
auto s = start_on(sch, work);
sync_wait(s);
The error in the code above is that the lambda passed to let_value
must return a sender. The error cannot be caught early because this is a dependent sender: the type of the delegation scheduler isn't known until we pass the sender to sync_wait
.
Compiling this with the µstdex library, which uses the suggested technique of propagating descriptive type errors via the completion signatures, results in the following diagnostic:
build] /home/eniebler/Code/ustdex/include/ustdex/detail/sync_wait.hpp:139:7: error: static_assert failed due to requirement '_is_completion_signatures<
[ustdex::ERROR<ustdex::WHERE (ustdex::IN_ALGORITHM, ustdex::let_value_t), ustdex::WHAT (ustdex::FUNCTION_MUST_RETURN_A_SENDER), ustdex::WITH_FUNCTION ((l
ambda at /home/eniebler/Code/ustdex/examples/scratch.cpp:64:25))>>'
build] static_assert(_is_completion_signatures<_completions>);
[build] ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[build] /home/eniebler/Code/ustdex/examples/scratch.cpp:74:12: note: in instantiation of function template specialization 'ustdex::sync_wait_t::operator
[()<ustdex::start_on_t::_sndr_t<ustdex::run_loop::_scheduler, ustdex::_let<ustdex::_value>::_sndr_t<ustdex::read_env_t::_sndr_t<ustdex::get_delegatee_sch
eduler_t>, (lambda at /home/eniebler/Code/ustdex/examples/scratch.cpp:64:25)>> &>' requested here
build] sync_wait(s);
[build] ^
[build] 1 error generated. [
This is the complete diagnostic. As you can see, the source of the error has been propagated out of the sender expression tree and reported at the API boundary, in sync_wait
. The diagnostic contains only the information the user needs to fix the problem.
For the authors of sender adaptor algorithms, this meta-error propagation technique presents a neigh insurmountable metaprogramming challenge. Computing completion signatures is hard enough. But now a child sender's completion signatures may not actually be completion signatures! They could instead be an error that the algorithm author must propagate in their completion signatures, or else lose the information about the root cause.
P2300 recognizes that manipulating completion signatures at compile time is taxing. It provides a utility to help: transform_completion_signatures
. Given a set of completion signatures and some alias templates, it applies transformations to the set, resulting in a new set of completion signatures. Sender adaptor authors can use transform_completion_signatures
to adapt the child sender's completions.
With a few small changes, transform_completions_signatures
can be made to automatically propagate any intermediate types that might represent errors, saving users the trouble of doing so manually. This paper proposes those design changes.
Consider the following use of transform_completion_signatures
:
struct ERROR_TOO_MANY_VALUES {};
template <class... Values>
using _value_completions_t = std::conditional_t<
sizeof...(Values) > 1,
ERROR_TOO_MANY_VALUES,set_value_t(Values...)>
completion_signatures<
>;
// For a given sender and environment, check that the value completions
// never send more than one value.
template <class Sndr, class Env>
using _checked_completions = transform_completion_signatures<
completion_signatures_of_t<Sndr, Env>,
completion_signatures<>,
_value_completions_t
>;
Type computation |
Result Before |
Result After |
---|---|---|
using S = decltype(just(1, 2, 3)); using T = _checked_completions<S, empty_env>; |
hard error |
|
using S = decltype(read_env(get_scheduler)); using T = _checked_completions<S, empty_env>; |
hard error |
ustdex::ERROR<ustdex::WHERE (ustdex::IN_ALGORITHM, ustdex::read_env_t), ustdex::WHAT (ustdex::THE_CURRENT_ENVIRONMENT_LACKS_THIS_QUERY), ustdex ::WITH_QUERY (ustdex::get_scheduler_t), ustdex::WITH_ENVIRONMENT (ustde x::env<>>) (with the µstdex library) |
In the first case, the type error happens in the _value_completions_t
alias template. In the second case, the type error happens when trying to ask the read_env(get_scheduler)
sender what its completions are when used with empty_env
. That's an error because the empty_env
does not have a value for the get_scheduler
query. In both cases, the error gets propagated by transform_completion_signatures
after the proposed change.
This proposed wording is based on P2300R9.
Change [async.ops]/13 as follows:
- A completion signature is a function type that describes a completion operation. An asychronous operation has a finite set of possible completion signatures corresponding to the completion operations that the asynchronous operation potentially evaluates ([basic.def.odr]). For a completion function
set
, receiverrcvr
, and pack of argumentsargs
, letc
be the completion operationset(rcvr, args...)
, and letF
be the function typedecltype(auto(set))(decltype((args))...)
. A completion signatureSig
is associated withc
if and only ifMATCHING-SIG(Sig, F)
istrue
([exec.general]). Together, a sender type and an environment typeEnv
determine the set of completion signatures of an asynchronous operation that results from connecting the sender with a receiver that has an environment of typeEnv
. The type of the receiver does not affect an asychronous operation’s completion signatures, only the type of the receiver’s environment. A sender type whose completion signatures are knowable independent of an execution environment is known as a non-dependent sender.
Change [exec.syn] as follows:
...template<class Sndr, class... Env
= empty_env> concept sender_in = see below; ...template<class Sndr, class... Env
= empty_env> requires sender_in<Sndr, Env...> using completion_signatures_of_t = call-result-t<get_completion_signatures_t, Sndr, Env...>; ...
Change [exec.snd.concepts] as follows:
template<class Sndr, class... Env= empty_env> concept sender_in = sender<Sndr> && (sizeof...(Env) <= 1) (queryable<Env> &&...) && requires (Sndr&& sndr, Env&&... env) { { get_completion_signatures( std::forward<Sndr>(sndr), std::forward<Env>(env)...) } -> valid-completion-signatures; };
this subtly changes the meaning of sender_in<Sndr>
. Before the change, it tests whether a type is a sender when used specifically with the environment empty_env
. After the change, it tests whether a type is a non-dependent sender. This is a stronger assertion to make about the type; it says that this type is a sender regardless of the environment. One can still get the old behavior with sender_in<Sndr, empty_env>
.
Change [exec.awaitables] as follows:
The sender concepts recognize awaitables as senders. For this clause ([exec]), an awaitable is an expression that would be well-formed as the operand of a
co_await
expression within a given context.For a subexpression
c
, letGET-AWAITER(c, p)
be expression-equivalent to the series of transformations and conversions applied toc
as the operand of an await-expression in a coroutine, resulting in lvaluee
as described by [expr.await]/3.2-4, wherep
is an lvalue referring to the coroutine’s promise type,Promise
. This includes the invocation of the promise type’sawait_transform
member if any, the invocation of theoperator co_await
picked by overload resolution if any, and any necessary implicit conversions and materializations. LetGET-AWAITER(c)
be expression-equivalent toGET-AWAITER(c, q)
whereq
is an lvalue of an unspecified empty class typenone-such
that lacks anawait_transform
member, and wherecoroutine_handle<none-such>
behaves ascoroutine_handle<void>
.Let
is-awaitable
be the following exposition-only concept:template<class T> concept await-suspend-result = see below; template<class A, class... Promise> concept is-awaiter = // exposition only requires (A& a, coroutine_handle<Promise...> h) { a.await_ready() ? 1 : 0; { a.await_suspend(h) } -> await-suspend-result; a.await_resume(); }; template<class C, class... Promise> concept is-awaitable = requires (C (*fc)() noexcept, Promise&... p) { { GET-AWAITER(fc(), p...) } -> is-awaiter<Promise...>; };
await-suspend-result<T>
istrue
if and only if one of the following istrue
:
T
isvoid
, orT
isbool
, orT
is a specialization ofcoroutine_handle
.For a subexpression
c
such thatdecltype((c))
is typeC
, and an lvaluep
of typePromise
,await-result-type<C, Promise>
denotes the typedecltype(GET-AWAITER(c, p).await_resume())
, andawait-result-type<C>
denotes the typedecltype(GET-AWAITER(c).await_resume())
.
Change [exec.getcomplsigs] as follows:
get_completion_signatures
is a customization point object. Letsndr
be an expression such thatdecltype((sndr))
isSndr
, and letenv
bean expression such thata pack of zero or one expressions. Thendecltype((env))
isEnv
get_completion_aignatures(sndr, env
is expression-equivalent to:...
)
remove_cvref_t<Sndr>::completion_signatures{}
if that expression is well-formed,
- Otherwise,
decltype(sndr.get_completion_signatures(env
if that expression is well-formed,...
)){}
Otherwise,remove_cvref_t<Sndr>::completion_signatures{}
if that expression is well-formed,Otherwise, if
is-awaitable<Sndr, env-promise<Env>
is...
>true
, then:completion_signatures< SET-VALUE-SIG(await-result-type<Sndr, env-promise<Env>...>), // see [exec.snd.concepts] set_error_t(exception_ptr), set_stopped_t()>{}Otherwise,
get_completion_signatures(sndr, env
is ill-formed....
)
If
get_completion_signatures(sndr)
is well-formed and its type denotes a specialization of thecompletion_signatures
class template, thenSndr
is a non-dependent sender type ([async.ops]).Given a type
Env
, ifcompletion_signatures_of_t<Sndr>
andcompletion_signatures_of_t<Sndr, Env>
are both well-formed and denote instantiations of thecompletion_signatures
class template, they shall denote the same set of completion signatures, with type equality determined withMATCHING-SIG
([exec.general]).
- Let
rcvr
be an rvalue receiver of typeRcvr
....
To [exec.adapt.general], add a paragraph (8) as follows:
- Unless otherwise specified, an adaptor whose child senders are all non-dependent ([async.ops]) is itself non-dependent. This requirement applies to any function that is selected by the implementation of the sender adaptor.
Change [exec.then] as follows:
- The names
then
,upon_error
, andupon_stopped
denote customization point objects. Forthen
,upon_error
, andupon_stopped
, letset-cpo
beset_value
,set_error
, andset_stopped
respectively. Let the expressionthen-cpo
be one ofthen
,upon_error
, orupon_stopped
. For subexpressionssndr
andf
, letSndr
bedecltype((sndr))
and letF
be the decayed type off
. IfSndr
does not satisfy sender, orF
does not satisfymovable-value
,then-cpo(sndr, f)
is ill-formed.
- Otherwise, let
invoke-result
be an alias template such thatinvoke-result<Ts...>
denotes the typeinvoke_result_t<F, Ts...>
. Ifsender_in<Sndr>
istrue
andgather-signatures<decayed-typeof<set-cpo>, completion_signatures_of_t<Sndr>, invoke-result, type-list>
is ill-formed, the program is ill-formed.
Otherwise, the expression
then-cpo(sndr, f)
is expression-equivalent to:.....
ForThe exposition-only class templatethen
,upon_error
, andupon_stopped
, letset-cpo
beset_value
,set_error
, andset_stopped
respectively.impls-for
([exec.snd.general]) is specialized forthen-cpo
as follows:....Change [exec.let] by inserting a new paragraph between (4) and (5) as follows:
- Let
invoke-result
be an alias template such thatinvoke-result<Ts...>
denotes the typeinvoke_result_t<F, Ts...>
. Ifsender_in<Sndr>
istrue
andgather-signatures<decayed-typeof<set-cpo>, completion_signatures_of_t<Sndr>, invoke-result, type-list>
is ill-formed, or if any of the types in the resulting type list fail to satisfysender
, the program is ill-formed.Change [exec.bulk] by inserting a new paragraph between (3) and (4) as follows:
- Let
invoke-result
be an alias template such thatinvoke-result<Ts...>
denotes the typeinvoke_result_t<F, Shape, Ts...>
. Ifsender_in<Sndr>
istrue
andgather-signatures<decayed-typeof<set-cpo>, completion_signatures_of_t<Sndr>, invoke-result, type-list>
is ill-formed, the program is ill-formed.Change [exec.split] as follows:
- The names
split
andensure_started
denote customization point objects. Let the expressionshared-cpo
be one ofsplit
orensure_started
. For a subexpressionsndr
, letSndr
bedecltype((sndr))
. Ifsender_in<Sndr, shared-env>
isfalse
,the program is ill-formed.shared-cpo(sndr)
Change [exec.start.detached] as follows:
start_detached
eagerly starts a sender without the caller needing to manage the lifetimes of any objects.The name
start_detached
denotes a customization point object. For a subexpressionsndr
, letSndr
bedecltype((sndr))
. Ifsender_in<Sndr, empty_env>
isfalse
,the program is ill-formed. Otherwise ...start_detached
Change [exec.sync.wait] as follows:
- The name
this_thread::sync_wait
denotes a customization point object. For a subexpressionsndr
, letSndr
bedecltype((sndr))
. Ifsender_in<Sndr, sync-wait-env>
isfalse
, theexpressionthe program is ill-formed. Otherwise,this_thread::sync_wait(sndr)
itthe expressionthis_thread::sync_wait(sndr)
is expression-equivalent to ...Change [exec.utils.tfxcmplsigs] as follows:
namespace std::execution { template<valid-completion-signaturesclass InputSignatures,valid-completion-signaturesclass AdditionalSignatures = completion_signatures<>, template<class...> class SetValue = default-set-value, template<class> class SetError = default-set-error,valid-completion-signaturesclass SetStopped = completion_signatures<set_stopped_t()>> using transform_completion_signatures = completion_signatures<see below>; }
SetValue
shall name an alias template such that for any template parameter packAs...
, the typeSetValue<As...>
is either ill-formed or elsevalid-completion-signatures<SetValue<As...>>
is satisfied.
SetError
shall name an alias template such that for any typeErr
,SetError<Err>
is either ill-formed or elsevalid-completion-signatures<SetError<Err>>
is satisfied.Then:
- If
valid-completion-signatures<E>
isfalse
whereE
is one ofInputSignatures
,AdditionalSignatures
, orSetStopped
,transform_completion_signatures<InputSignatures, AdditionalSignatures, SetValue, SetError, SetStopped>
denotes the typeE
. If there are multiple types that fail to satisfyvalid-completion-signatures
, it is unspecified which is chosen.
Let
Vs
be a pack of the types in the...type-list
named bygather-signatures<set_value_t, InputSignatures, SetValue, type-list>
.Let
Es
be a pack of the types in the...type-list
named bygather-signatures<set_error_t, InputSignatures, type_identity_t, error-list>
, whereerror-list
is an alias template such thaterror-list<Ts...>
namestype-list<SetError<Ts>...>
.Let
Ss
name the typecompletion_signatures<>
ifgather-signatures<set_stopped_t, InputSignatures, type-list, type-list>
is an alias for the typetype-list<>
; otherwise,SetStopped
.Then:
- If any of the types in
Vs
orEs
are ill-formed, thentransform_completion_signatures<InputSignatures, AdditionalSignatures, SetValue, SetError, SetStopped>
is ill-formed,
- Otherwise, if any type
E
from set of types inVs
andEs
fails to satisfyvalid-completion-signatures
, thentransform_completion_signatures<InputSignatures, AdditionalSignatures, SetValue, SetError, SetStopped>
denotes the typeE
. If more than one type inVs
andEs
fail to satisfyvalid-completion-signatures
, it is unspecified which is chosen.
- Otherwise,
transform_completion_signatures<InputSignatures, AdditionalSignatures, SetValue, SetError, SetStopped>
names the typecompletion_signatures<Sigs...>
whereSigs...
is the unique set of types in all the template arguments of all thecompletion_signatures
specializations in[AdditionalSignatures, Vs..., Es..., Ss]
. For the purpose of uniqueness, type equality is determined withMATCHING-SIG
([exec.general]).Acknowlegments
We owe our thanks to Ville Voutilainen who first noticed that most sender expressions could be type-checked eagerly but are not by P2300R8.