1. Scope
We were asked by the chair of LEWG to review how the Executors proposal [P0443R13] makes use of Properties [P1393]. We were to look at all uses of properties within P0443, not just the sections that define properties.
We did not review P1393, where the properties mechanism is defined. But we did spend a fair amount of time reading and understanding P1393, and in the process we noticed a couple of issues in that paper, and we have offered suggested fixes.
The review team varied some during the effort, but ended up consisting of David Olsen, Ruslan Arutyunyan, Michael J. Voss, and Michał Dominiak, with contributions from the polymorphic executor review team led by Inbal Levi. Michael and Ruslan wrote all the examples. We were assisted in all our efforts by three of the many authors of P0443, Chris Kohlhoff, Daisy Hollman, and Kirk Shoop. The authors answered many questions, provided lots of background information, made useful suggestions, and were generally very helpful. In particular, Chris Kohlhoff’s implementation of the properties mechanism and some other parts of executors that are part of [Asio] (see also here and here) made it much easier to write sample code and try out ideas.
2. Introduction
Properties were originally suggested as a way to simplify the configuration and customization of the 
The properties mechanism was deemed to be useful beyond executors, so it was separated into its own paper, [P1393], and moved from namespace 
3. Examples
We provide a set of examples that begin with a simple use of a standard property and standard executor. We then introduce a custom executor and show how support for a standard property can be added to that executor. We next implement a custom property and show how it can be used with a standard executor as well as our custom executor. Throughout the discussion we also provide example uses of 
All of the examples were compiled against the [Asio] implementation of executors, so the code refers to many things in the 
3.1. Using standard properties
The example that follows shows a simple use of 
void example_with_stp () { asio :: static_thread_pool stp ( 8 ); // require is ok, since static_thread_pool supports blocking.always auto ex = asio :: require ( stp . executor (), asio :: execution :: blocking . always ); std :: cout << "Required blocking.always" << std :: endl ; // query is ok and informative, since static_thread_pool supports blocking_t if ( asio :: query ( ex , asio :: execution :: blocking ) == asio :: execution :: blocking . possibly ) std :: cout << "ex blocking.possibly" << std :: endl ; int i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( ex , [ & i ]() { if ( i == 1 ) i = 2 ; }); std :: cout << "Before wait i == " << i << std :: endl ; stp . wait (); std :: cout << "After wait i == " << i << std :: endl ; } 
Since the executor provided by 
Required blocking . always Before wait i == 2 After wait i == 2 
3.1.1. Writing a custom executor
It is relatively straightforward to add support for a standard property to a custom executor. Below we show a custom 
namespace toy { class toy_tbb_context { public : class executor_ { public : constexpr asio :: execution :: blocking_t query ( asio :: execution :: blocking_t ) const noexcept { return blocking_value_ ; } auto require ( asio :: execution :: blocking_t :: always_t val ) const noexcept { return executor_ { context_ , val }; } bool operator == ( const executor_ & other ) const noexcept { return this == & other ; } bool operator != ( const executor_ & other ) const noexcept { return this != & other ; } executor_ ( const executor_ & e ) = default ; ~ executor_ () = default ; template < typename Invocable > void execute ( Invocable && f ) const { if ( blocking_value_ == asio :: execution :: blocking_t :: always ) { context_ . task_group_ . run_and_wait ( std :: forward < Invocable > ( f )); } else { context_ . task_group_ . run ( std :: forward < Invocable > ( f )); } } private : friend toy_tbb_context ; asio :: execution :: blocking_t blocking_value_ ; toy_tbb_context & context_ ; executor_ ( toy_tbb_context & context ) noexcept : context_ ( context ) {} executor_ ( toy_tbb_context & context , const asio :: execution :: blocking_t & blocking_val ) noexcept : context_ ( context ), blocking_value_ ( blocking_val ) {} }; executor_ executor () { return executor_ { * this }; } void wait () { task_group_ . wait (); } private : tbb :: task_group task_group_ ; }; } 
We can replace the 
void example_with_toy_tbb () { toy :: toy_tbb_context ttc ; // require is ok, since toy_tbb_context supports blocking.always auto ex = asio :: require ( ttc . executor (), asio :: execution :: blocking . always ); // query is ok and informative, since toy_tbb_context supports blocking_t if ( asio :: query ( ex , asio :: execution :: blocking ) == asio :: execution :: blocking . possibly ) std :: cout << "ex blocking.possibly" << std :: endl ; int i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( ex , [ & i ]() { if ( i == 1 ) i = 2 ; }); std :: cout << "Before wait i == " << i << std :: endl ; ttc . wait (); std :: cout << "After wait i == " << i << std :: endl ; } 
It should be noted that executors, senders and schedulers are not required to support all of the standard properties. We implemented a 
namespace toy { class toy_tbb_context_no_support { public : class executor_ { public : bool operator == ( const executor_ & other ) const noexcept { return this == & other ; } bool operator != ( const executor_ & other ) const noexcept { return this != & other ; } executor_ ( const executor_ & e ) = default ; ~ executor_ () = default ; template < typename Invocable > void execute ( Invocable && f ) const { context_ . task_group_ . run ( std :: forward < Invocable > ( f )); } private : toy_tbb_context_no_support & context_ ; friend toy_tbb_context_no_support ; executor_ ( toy_tbb_context_no_support & context ) noexcept : context_ ( context ) {} }; executor_ executor () { return executor_ { * this }; } void wait () { task_group_ . wait (); } private : tbb :: task_group task_group_ ; }; } 
We can no longer plug this context in directly for 
void example_with_toy_tbb_no_support () { toy :: toy_tbb_context_no_support ttc ; // prefer is ok, even for an executor that does not explicitly support blocking_t // it returns an executor with the same properties auto ex = asio :: prefer ( ttc . executor (), asio :: execution :: blocking . always ); std :: cout << "Preferred blocking.always" << std :: endl ; // query is ok, even for an executor that does not explicitly support blocking_t // the result is broad but still useful if ( asio :: query ( ex , asio :: execution :: blocking ) == asio :: execution :: blocking . possibly ) std :: cout << "ex blocking.possibly" << std :: endl ; std :: atomic < int > i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( ex , [ & i ]() { int j = 1 ; i . compare_exchange_strong ( j , 2 ); }); std :: cout << "Before wait i == " << i << std :: endl ; ttc . wait (); std :: cout << "After wait i == " << i << std :: endl ; } 
Since, without blocking, our example now contains a real race, we use an 
Preferred blocking . always ex blocking . possibly Before wait i == 0 After wait i == 1 
3.1.2. Using any_executor 
   We can wrap the executor types returned by 
using any_exec_type = asio :: execution :: any_executor < asio :: execution :: blocking_t , asio :: execution :: blocking_t :: always_t > ; void algorithm ( any_exec_type ex0 ) { // since blocking_t::always_t is supported, we know we can require it auto ex = asio :: require ( ex0 , asio :: execution :: blocking . always ); std :: cout << "Required blocking.always" << std :: endl ; int i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); // Since it’s blocking, we don’t need the compare_exchange asio :: execution :: execute ( ex , [ & i ]() { if ( i == 1 ) { i = 2 ; }}); std :: cout << "i == " << i << std :: endl ; } void example_with_stp () { asio :: static_thread_pool stp ( 8 ); algorithm ( stp . executor ()); stp . wait (); } void example_with_toy_tbb () { toy :: toy_tbb_context ttc ; algorithm ( ttc . executor ()); ttc . wait (); } 
Our 
using any_exec_ptype = asio :: execution :: any_executor < asio :: execution :: blocking_t , asio :: execution :: prefer_only < asio :: execution :: blocking_t :: always_t >> ; void algorithm_with_prefer ( any_exec_ptype ex0 ) { // blocking_t::always_t might be supported, but we don’t know, so prefer auto ex = asio :: prefer ( ex0 , asio :: execution :: blocking . always ); std :: cout << "Preferred blocking.always" << std :: endl ; if ( asio :: query ( ex , asio :: execution :: blocking ) == asio :: execution :: blocking . always ) { int i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( ex , [ & i ]() { if ( i == 1 ) { i = 2 ; }}); std :: cout << "i == " << i << std :: endl ; } else { std :: atomic < int > i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( ex , [ & i ]() { int j = 1 ; i . compare_exchange_strong ( j , 2 ); }); std :: cout << "i == " << i << std :: endl ; } } void example_with_toy_tbb_no_support () { toy :: toy_tbb_context_no_support ttc ; algorithm_with_prefer ( ttc . executor ()); ttc . wait (); } 
Since 
3.2. Using custom properties
It is also fairly straightforward to create a custom property. To demonstrate this, we implemented a true if tracing is on and false if tracing is off. We provide a free function 
namespace toy { struct tracing_t { tracing_t () = default ; constexpr tracing_t ( bool val ) : value_ ( val ) {} template < typename T > static constexpr bool is_applicable_property_v = asio :: execution :: executor < T > ; static constexpr bool is_requirable = true; static constexpr bool is_preferable = true; using polymorphic_query_result_type = bool ; constexpr explicit operator bool () const { return value_ ; } private : bool value_ { false}; }; inline constexpr tracing_t tracing ; template < typename E > constexpr bool query ( const E & , const tracing_t & ) { return false; } } 
3.2.1. Modifying our custom executor
The support in our custom 
namespace toy { class toy_tbb_context { public : class executor_ { public : // ... constexpr bool query ( const tracing_t & ) const noexcept { return static_cast < bool > ( tracing_value_ ); auto require ( const tracing_t & val ) const noexcept { return executor_ { context_ , blocking_value_ , val }; } // ... template < typename Invocable > void execute ( Invocable && f ) const { if ( tracing_value_ ) ++ context_ . executions_ ; if ( blocking_value_ == asio :: execution :: blocking_t :: always ) { context_ . task_group_ . run_and_wait ( std :: forward < Invocable > ( f )); } else { context_ . task_group_ . run ( std :: forward < Invocable > ( f )); } } private : // ... tracing_t tracing_value_ ; // ... executor_ ( toy_tbb_context & context , const asio :: execution :: blocking_t & blocking_val , const tracing_t & tracing_val ) noexcept : context_ ( context ), blocking_value_ ( blocking_val ), tracing_value_ ( tracing_val ) {} }; // ... int traced_executions () noexcept { return executions_ ; } private : // ... std :: atomic < int > executions_ ; }; } 
We can now 
void example_with_toy_tbb () { toy :: toy_tbb_context ttc ; // require is ok, since toy_tbb_context supports blocking.always auto ex = asio :: require ( ttc . executor (), asio :: execution :: blocking . always ); std :: cout << "Required blocking.always" << std :: endl ; // query is ok and informative, since toy_tbb_context supports blocking_t if ( asio :: query ( ex , asio :: execution :: blocking ) == asio :: execution :: blocking . possibly ) std :: cout << "ex blocking.possibly" << std :: endl ; int i = 0 ; asio :: execution :: execute ( ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( ex , [ & i ]() { if ( i == 1 ) i = 2 ; }); ttc . wait (); std :: cout << "i == " << i << std :: endl ; std :: cout << "traced_executions == " << ttc . traced_executions () << std :: endl ; i = 0 ; auto tex = asio :: require ( ex , toy :: tracing_t { true}); std :: cout << "Required toy::tracing_t{true}" << std :: endl ; asio :: execution :: execute ( tex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( tex , [ & i ]() { if ( i == 1 ) i = 2 ; }); ttc . wait (); std :: cout << "i == " << i << std :: endl ; std :: cout << "traced_executions == " << ttc . traced_executions () << std :: endl ; } 
The output of the above code is as follows:
Required blocking . always i == 2 traced_executions == 0 Required toy :: tracing_t { true} i == 2 traced_executions == 2 
void example_with_stp () { asio :: static_thread_pool stp ( 8 ); // require is ok, since static_thread_pool supports blocking.always auto ex = asio :: require ( stp . executor (), asio :: execution :: blocking . always ); std :: cout << "Required blocking.always" << std :: endl ; // query is ok and informative, since static_thread_pool supports blocking_t if ( asio :: query ( ex , asio :: execution :: blocking ) == asio :: execution :: blocking . possibly ) std :: cout << "ex blocking.possibly" << std :: endl ; int i = 0 ; // prefer is ok, even for an executor that does not explicitly support toy::tracing_t // it returns an executor with the same properties auto tex = asio :: prefer ( ex , toy :: tracing_t { true}); std :: cout << "Preferred toy::tracing_t{true}" << std :: endl ; asio :: execution :: execute ( tex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( tex , [ & i ]() { if ( i == 1 ) i = 2 ; }); stp . wait (); std :: cout << "i == " << i << std :: endl ; } 
3.2.2. Using any_executor 
   Lastly, we can wrap both 
using any_exec_type = asio :: execution :: any_executor < asio :: execution :: blocking_t , asio :: execution :: prefer_only < toy :: tracing_t > , asio :: execution :: blocking_t :: always_t > ; using any_almost_same_exec_type = asio :: execution :: any_executor < asio :: execution :: blocking_t , asio :: execution :: blocking_t :: always_t , asio :: execution :: prefer_only < toy :: tracing_t >> ; void algorithm ( any_exec_type ex ) { auto tt_ex = asio :: require ( ex , asio :: execution :: blocking . always ); std :: cout << "Required blocking.always" << std :: endl ; if ( asio :: query ( tt_ex , toy :: tracing )) std :: cout << "Using tracing" << std :: endl ; int i = 0 ; asio :: execution :: execute ( tt_ex , [ & i ]() { i = 1 ; }); asio :: execution :: execute ( tt_ex , [ & i ]() { if ( i == 1 ) i = 2 ; }); std :: cout << "i == " << i << std :: endl ; } void algorithm_tracing ( any_almost_same_exec_type ex ) { auto tracing_exec = asio :: prefer ( ex , toy :: tracing_t { true}); algorithm ( tracing_exec ); } void combined_example () { toy :: toy_tbb_context ttc ; algorithm ( ttc . executor ()); std :: cout << "traced_executions == " << ttc . traced_executions () << std :: endl ; algorithm_tracing ( ttc . executor ()); std :: cout << "traced_executions == " << ttc . traced_executions () << std :: endl ; asio :: static_thread_pool stp ( 4 ); algorithm ( stp . executor ()); std :: cout << "traced_executions == " << ttc . traced_executions () << std :: endl ; algorithm_tracing ( stp . executor ()); std :: cout << "traced_executions == " << ttc . traced_executions () << std :: endl ; } 
When run, this example turns on tracing for the 
Required blocking . always i == 2 traced_executions == 0 Required blocking . always Using tracing i == 2 traced_executions == 2 Required blocking . always i == 2 traced_executions == 2 Required blocking . always i == 2 traced_executions == 2 
4. Small Issues
For small issues that have an obvious resolution, mostly wording bugs, we created issues in the Executors GitHub repository. The GitHub issues are listed here, but they can be resolved easily by the authors of P0443 and don’t need to be discussed in LEWG.
- 
     any_executor section is in the wrong place #488 
- 
     Use of undefined name "Property" in behavioral properties #489 
- 
     Wrong property type in static_thread_pool sender and executor summaries #490 
- 
     Incorrect syntax in description of allocator_t<ProtoAllocator> property #491 
- 
     bad_executor refers to non-existent any_executor::bulk_execute #493 
- 
     S::Ei should be S::Ni #494 
- 
     prefer_only example is out of date #498 
5. Issues
Here is a list of issue that we think deserve more discussion in LEWG. Some of the issues have a suggested resolution which we would like LEWG to approve and the P0443 authors to implement. Some issues merely identify a problem that we would like the authors of P0443 to solve, possibly with guidance from LEWG. Any changes made in response to § 5.2 Generic blocking adapter is not implementable or § 5.7 bulk_guarantee specification should match execution policies should also be reviewed by SG1.
5.1. Should properties be usable with non-executors?
All of the properties defined in P0443 have
This limits the property to types that satisfy thetemplate < class T > static constexpr bool is_applicable_property_v = executor < T > ; 
executor executor query require prefer std :: prefer If the
If
is not a well-formed constant expression with valueis_applicable_property_v < T , Prop0 > && Prop0 :: is_preferable true,is ill-formed.std :: prefer ( E , P0 , Pn ...) 
is_applicable_property_v std :: prefer E The specification for the 
Some senders satisfy the 
It seems that it would be useful to be able to apply these properties to senders.  Senders have access to an executor under the covers.  When users get the sender from 
Similar reasoning applies to schedulers. Schedulers dole out senders which are associated with executors. It would be helpful for users to tell the scheduler what properties they want for their executors, and the scheduler would only create senders that are associated with executors with those properties.
One possible solution to this discrepancy is to change the 
The other way for senders and schedulers to support these properties is to specialize 
The inconsistency in the use of properties by 
- 
     Remove the support for properties, deleting the query require 
- 
     Change the is_applicable_property_v executor < T > || sender < T > || scheduler < T > 
- 
     Change the is_applicable_property_v true. Some members of our group think that using a property defined in theexecution 
- 
     Specify that std :: is_applicable_property < T , P > true_type 
The review group agreed that this is an issue that needs to be addressed, but could not agree on the best way to address it.  There was some support for all options other than option #1.  In the current [Asio] implementation of 
Even though properties should already work as specified for 
5.2. Generic blocking adapter is not implementable
The specification for 
This customization uses an adapter to implement theproperty.blocking_adaptation_t :: allowed_t template < class Executor > friend see - below require ( Executor ex , blocking_adaptation_t :: allowed_t ); 
This allows the 
The specification for 
If the executor has theproperty, this customization uses an adapter to implement theblocking_adaptation_t :: allowed_t property.blocking_t :: always_t template < class Executor > friend see - below require ( Executor ex , blocking_t :: always_t ); 
This allows the 
The way these two properties are specified, it is possible to turn any executor into a blocking executor:
// This should work with almost any executor executor auto blocking_ex = std :: require ( std :: require ( ex , blocking_adaptation . allowed ), blocking . always ); static_assert ( std :: query ( blocking_ex , blocking ) == blocking . always ); 
(It is possible for executor types to prevent these properties from being applied, but it takes extra work on the part of the executor.  If executor type 
This is a problem because it is not possible to implement a blocking wrapper around every executor. Without understanding the details of the wrapped executor’s execution context, the wrapper can’t choose a synchronization primitive that will be known to work between the current thread and the wrapped executor’s execution context. For example, using a mutex or a condition variable for synchronization won’t work if the execution context is a fiber or certain GPUs. The standard properties for executors don’t provide enough information about the execution context for the wrapper to know the proper way to block.
A standard construct that makes it easy for users to write code that won’t work is poor design. Changes need to be made to the blocking_adaptation property. Here are some options. Some of them can be combined together.
- 
     Remove blocking_adaptation blocking . always std :: require ( Ex , blocking_t :: always_t ) blocking . always blocking_t :: always_t :: is_preferable falsetotrue.)
- 
     Remove the generic blocking_adaptation . allowed blocking_adaptation blocking . always blocking_adaptation . allowed blocking . allowed blocking_adaptation . allowed blocking . always blocking_adaptation . allowed std :: require ( Ex , blocking_t :: always_t ) 
- 
     The blocking . always blocking_adaptation_t :: allowed_t mapping_t :: thread_t mapping_t :: new_thread_t 
- 
     Change the name of blocking_adaptation blocking thread_blocking_adaptation thread :: blocking_adaptation 
We recommend option #1, which had the support of the majority of the review group.
If the committee feels strongly that a generic adapter for 
5.3. Non-movable properties
[P1393] does not require properties to be movable or copyable.  This was an intentional decision, and not an oversight.  While all the properties defined in P0443 are movable and copyable, user-defined executors may define custom properties that are not movable or copyable.  The executors framework, such as 
In 
template < class Property > any_executor require ( const Property & p ) const ; 
(We have heard that the 
In several places in this section the operationis used. All such uses mean the first typeFIND_CONVERTIBLE_PROPERTY ( p , pn ) in the parameter packP for whichpn isstd :: is_same_v < p , P > trueorisstd :: is_convertible_v < p , P > true. If no such typeexists, the operationP is ill-formed.FIND_CONVERTIBLE_PROPERTY ( p , pn ) 
Chris Kohlhoff has tested this and has found these changes to be necessary and sufficient to support non-movable properties in 
Changing 
template < class InnerProperty > struct prefer_only { InnerProperty property ; 
The data member 
Even though end users should never have to create 
5.4. Polymorphic executor wrappers and prefer-only properties
The specification of customization point 
(
This change must be made, even if the other proposed changes in this section are rejected.)
- Otherwise,
ifE .N == 0 
This specification of 
As an example, assume that some property 
maybe_nice_exec e = ...; auto nice_e = std :: prefer ( e , prefer_nice ); 
the executor 
Now assume that we wrap a 
maybe_nice_exec e = ...; any_executor < prefer_nice_t > wrapped { e }; auto nice_wrapped = std :: prefer ( wrapped , prefer_nice ); 
Given the current specification of 
Returns: A polymorphic wrapper whose target is the result ofThe call to, wherestd :: require ( e , p ) is the target object ofe .* this 
std :: require ( e ,  p ) is_requirable maybe_nice_exec std :: prefer any_executor any_executor prefer_nice_t To get the correct result, 
When generic code such as 
The review group recommends that this issue be fixed by changing the specification of 
The namedenotes a customization point object. The expressionprefer for some subexpressionsstd :: prefer ( E , P0 , Pn ...) andE , and whereP0 representsPn ... subexpressions (whereN is 0 or more, and with typesN andT = decay_t < decltype ( E ) > ) is expression-equivalent to:Prop0 = decay_t < decltype ( P0 ) > 
If
is not a well-formed constant expression with valueis_applicable_property_v < T , Prop0 > && Prop0 :: is_preferable true,is ill-formed.std :: prefer ( E , P0 , Pn ...) 
Otherwise,
ifE and the expressionN == 0 is a well-formed constant expression with valueProp0 :: template static_query_v < T > == Prop0 :: value () true.
Otherwise,
if( E ). require ( P0 ) and the expressionN == 0 is a valid expression.( E ). require ( P0 ) 
Otherwise,
ifrequire ( E , P0 ) and the expressionN == 0 is a valid expression with overload resolution performed in a context that does not include the declaration of therequire ( E , P0 ) customization point object.require - Otherwise,
if( E ). prefer ( P0 ) and the expressionN == 0 is a valid expression.( E ). prefer ( P0 ) - Otherwise,
ifprefer ( E , P0 ) and the expressionN == 0 is a valid expression with overload resolution performed in a context that does not include the declaration of theprefer ( E , P0 ) customization point object.prefer - Otherwise,
ifE .N == 0 
Otherwise,
ifstd :: prefer ( std :: prefer ( E , P0 ), Pn ...) and the expressionN > 0 is a valid expression.std :: prefer ( std :: prefer ( E , P0 ), Pn ...) 
Otherwise,
is ill-formed.std :: prefer ( E , P0 , Pn ...) 
Along with this change, 
Please note that only generic code that wants to correctly forward 
This is the preferred solution for [Asio], which has already implemented this change.
5.5. any_executor FIND_CONVERTIBLE_PROPERTY 
   The type-erased executor wrapper 
In several places in this section the operationis used. All such uses mean the first typeFIND_CONVERTIBLE_PROPERTY ( p , pn ) in the parameter packP for whichpn isstd :: is_convertible_v < p , P > true. If no such typeexists, the operationP is ill-formed.FIND_CONVERTIBLE_PROPERTY ( p , pn ) 
This exposition-only expression is used in the specification of 
template < class Property > any_executor require ( Property p ) const ; Remarks: This function shall not participate in overload resolution unless
is well-formed and has the valueFIND_CONVERTIBLE_PROPERTY ( Property , SupportableProperties ) :: is_requirable true.Returns: A polymorphic wrapper whose target is the result of
, wherestd :: require ( e , p ) is the target object ofe .* this 
Consider this example:
any_executor < blocking_t , blocking_t :: never_t > ex { my_executor }; ex . require ( blocking . never ); 
The 
The fix is to check for 
template < class Property > any_executor require ( const Property & p ) const ; Letbe the first typeFIND_REQUIRABLE_PROPERTY ( p , pn ) in the parameter packP for whichpn 
isis_same_v < p , P > trueorisis_convertible_v < p , P > true, and
isP :: is_requirable true.If no such
exists, the operationP is ill-formed.FIND_REQUIRABLE_PROPERTY ( p , pn ) Remarks: This function shall not participate in overload resolution unless
FIND_CONVERTIBLE_PROPERTY FIND_REQUIRABLE_PROPERTY ( Property , SupportableProperties ) is well-formed:: is_requirable and has the value.trueReturns: A polymorphic wrapper whose target is the result of
, wherestd :: require ( e , p ) is the target object ofe .* this 
A similar change also needs to be made to 
template < class Property , class ... SupportableProperties > any_executor prefer ( const any_executor < SupportableProperties ... >& e , const Property & p ); Letbe the first typeFIND_PREFERABLE_PROPERTY ( p , pn ) in the parameter packP for whichpn 
isis_same_v < p , P > trueorisis_convertible_v < p , P > true, and
isP :: is_preferable true.If no such
exists, the operationP is ill-formed.FIND_PREFERABLE_PROPERTY ( p , pn ) Remarks: This function shall not participate in overload resolution unless
FIND_CONVERTIBLE_PROPERTY FIND_PREFERABLE_PROPERTY ( Property , SupportableProperties ) is well-formed:: is_preferable and has the value.trueReturns: A polymorphic wrapper whose target is the result of
, wherestd :: prefer ( e , p ) is the target object ofe .* this 
5.6. any_executor < P ... >:: target 
   The member functions 
Using a property rather than a member function to retrieve the target executor from an 
struct target_property_t { using polymorphic_query_result_type = std :: any ; static constexpr bool is_requirable = false; static constexpr bool is_preferable = false; template < typename Ex > static constexpr bool is_applicable_property_v = std :: execution :: executor < Ex > ; }; template < typename Executor > struct is_polymorphic_executor : std :: false_type {}; template < typename ... Ps > struct is_polymorphic_executor < std :: execution :: any_executor < Ps ... >> : std :: true_type {}; template < typename Executor > inline constexpr bool is_polymorphic_executor_v = is_polymorphic_executor < Executor >:: value ; template < typename Executor > std :: any query ( Executor ex , target_property_t ) { static_assert ( ! is_polymorphic_executor_v < Executor > , "tried to query the target of a polymorphic executor which does not support target_property_t" ); return std :: any ( ex ); } 
(This proof of concept, with some example code of how to use it, can be found here.)
If the committee is not overly concerned about the RTTI overhead, the specification can be left unchanged.  If the committee likes this approach of using a property to retrieve the target executor, then the 
5.7. bulk_guarantee 
   The specification 
- 
     bulk_guarantee . unsequenced 
- 
     bulk_guarantee . sequenced 
- 
     bulk_guarantee . parallel 
These correspond in some ways to the execution policies for parallel algorithms, except that there are four execution policies, and 
The relationship between the 
Daisy Hollman thinks it could be helpful to move the specification of 
5.8. What does "established property" mean?
Many places in the paper use the phrase "property already established" or something similar. But there is no precise definition of what "established" means. The paper should provide a definition, so that implementations (and users who write their own executor types) have a better understanding of what to implement.
Two obvious possible definitions of an established property might be:
- 
     std :: query ( ex , prop ) == prop_value 
- 
     std :: require ( ex , prop ) == ex 
Both of those definitions have flaws, and don’t work correctly in all situations. We don’t have a good suggestion of what the definition of an established property should be. Like the previous issue, we think this is something that the P0443 authors need to address.
(We have heard that the 
5.9. allocator_t :: value 
   Section 2.2.13.1 "
static constexpr ProtoAllocator value () const ; Returns: The exposition-only member
.a_ 
We think the correct fix is to change 
5.10. context 
   The 
The issue is that there are no requirements on the type or the value of the result of querying the 
We are not proposing that P0443 be changed to deal with this, since we didn’t think of any way to improve the situation. We are just raising this as an issue to be aware of.