From e7fb312acc459e8fbabbc91474f50a9bbede9152 Mon Sep 17 00:00:00 2001 From: Eric Niebler Date: Tue, 25 Jun 2024 23:57:48 -0700 Subject: [PATCH] feedback from LWG wording review, 2024-06-25 --- execution.bs | 576 ++++++++++++++++++++++++++++----------------------- 1 file changed, 313 insertions(+), 263 deletions(-) diff --git a/execution.bs b/execution.bs index 713f606..74f723d 100644 --- a/execution.bs +++ b/execution.bs @@ -5965,7 +5965,7 @@ namespace std::execution { MANDATE-NOTHROW(as_const(env).query(get_allocator)). * Mandates: If the expression above is well-formed, its type - satisfies *`simple-allocator`* ([allocator.requirements.general]). + satisfies `simple-allocator` ([allocator.requirements.general]). 3. `forwarding_query(get_allocator)` is a core constant expression and has value `true`. @@ -6182,7 +6182,7 @@ namespace std::execution { 2. Let `Sch` be the type of a scheduler and let `Env` be the type of an execution environment for which `sender_in, Env>` is - satisfied. Then sender-of-in<schedule_result_t<Sch>, + satisfied. Then sender-in-of<schedule_result_t<Sch>, Env> shall be modeled. 3. None of a scheduler's copy constructor, destructor, equality comparison, or @@ -6360,21 +6360,21 @@ namespace std::execution { 3. This subclause makes use of the following exposition-only entities. 1. For a queryable object `env`, FWD-ENV(env) is an - expression whose type satisfies *`queryable`* such that for a query object + expression whose type satisfies `queryable` such that for a query object `q` and a pack of subexpressions `as`, the expression FWD-ENV(env).query(q, as...) is ill-formed if `forwarding_query(q)` is `false`; otherwise, it is expression-equivalent to `env.query(q, as...)`. 2. For a query object `q` and a subexpression `v`, MAKE-ENV(q, - v) is an expression `env` whose type satisfies *`queryable`* such + v) is an expression `env` whose type satisfies `queryable` such that the result of `env.query(q)` has a value equal to `v` ([concepts.equality]). Unless otherwise stated, the object to which `env.query(q)` refers remains valid while `env` remains valid. 3. For two queryable objects `env1` and `env2`, a query object `q` and a pack of subexpressions `as`, JOIN-ENV(env1, env2) is - an expression `env3` whose type satisfies *`queryable`* such that + an expression `env3` whose type satisfies `queryable` such that `env3.query(q, as...)` is expression-equivalent to: - `env1.query(q, as...)` if that expression is well-formed, @@ -6390,13 +6390,13 @@ namespace std::execution { arguments. 5. For a scheduler `sch`, SCHED-ATTRS(sch) is an - expression `o1` whose type satisfied *`queryable`* such that + expression `o1` whose type satisfies `queryable` such that o1.query(get_completion_scheduler<Tag>) is a expression with the same type and value as `sch` where `Tag` is one of `set_value_t` or `set_stopped_t`, and such that o1.query(get_domain) is expression-equivalent to sch.query(get_domain). SCHED-ENV(sch) - is an expression `o2` whose type satisfied *`queryable`* such that + is an expression `o2` whose type satisfies `queryable` such that o1.query(get_scheduler) is a prvalue with the same type and value as `sch`, and such that o2.query(get_domain) is expression-equivalent to sch.query(get_domain). @@ -6460,8 +6460,8 @@ namespace std::execution { constexpr auto get-domain-early(const Sndr& sndr) noexcept; - 1. Effects: Equivalent to: return Domain(); - where `Domain` is the decayed type of the first of the + 1. Effects: Equivalent to: return Domain(); + where `Domain` is the decayed type of the first of the following expressions that is well-formed: - `get_domain(get_env(sndr))` @@ -6492,14 +6492,14 @@ namespace std::execution { with `schedule_from` ([exec.schedule.from])) to give scheduler authors a way to customize both how to transition onto (`transfer`) and off of (`schedule_from`) a given execution - context. Thus, `transfer` must ignore the domain of the - predecessor and use the domain of the destination scheduler to - select a customization, a property that is unique to `transfer`. - That is why it is given special treatment here. + context. Thus, `transfer` ignores the domain of the predecessor + and uses the domain of the destination scheduler to select a + customization, a property that is unique to `transfer`. That is + why it is given special treatment here. - - Otherwise, return Domain(); where `Domain` is + - Otherwise, return Domain(); where `Domain` is the first of the following expressions that is well-formed and - its type is not `void`: + whose type is not `void`: - `get_domain(get_env(sndr))` @@ -6540,17 +6540,19 @@ namespace std::execution { 13.
-        template<class... T>
+        template<class T0, class T1, ... class Tn>
         struct product-type {  // exposition only
-          using type0 = T0;      // exposition only
-          using type1 = T1;      // exposition only
-            ...
-          using typen-1 = Tn-1;   // exposition only
-
           T0 t0;      // exposition only
           T1 t1;      // exposition only
             ...
-          Tn-1 tn-1;   // exposition only
+          Tn tn;      // exposition only
+
+          template<size_t I, class Self>
+          constexpr decltype(auto) get(this Self&& self) noexcept; // exposition only
+
+          template<class Self, class Fn>
+          constexpr decltype(auto) apply(this Self&& self, Fn&& fn) // exposition only
+            noexcept(see below);
         };
         
@@ -6558,7 +6560,35 @@ namespace std::execution { `product-type` is usable as the initializer of a structured binding declaration [dcl.struct.bind]. - + 2.
+            template<size_t I, class Self>
+            constexpr decltype(auto) get(this Self&& self) noexcept;
+            
+ + 1. *Effects:* Equivalent to: + +
+                auto& [...ts] = self;
+                return std::forward_like<Self>(ts...\[I]);
+                
+ + 3.
+            template<class Self, class Fn>
+            constexpr decltype(auto) apply(this Self&& self, Fn&& fn) noexcept(see below);
+            
+ + 1. *Effects:* Equivalent to: + +
+                auto& [...ts] = self;
+                return std::forward<Fn>(fn)(std::forward_like<Self>(ts)...);
+                
+ + 2. *Requires:* The expression in the `return` statement above is + well-formed. + + 4. *Remarks:* The expression in the `noexcept` clause is `true` if the + `return` statement above is not potentially throwing; otherwise, `false`. 14.
         template<class Tag, class Data = see below, class... Child>
@@ -6574,11 +6604,8 @@ namespace std::execution {
             - `(sender &&...)`
 
         2. *Returns:* A prvalue of type basic-sender<Tag,
-            decay_t<Data>, decay_t<Child>...> where the
-            `tag` member has been default-initialized and the
-            `data` and
-            childn... members have been direct
-            initialized from their respective forwarded arguments, where
+            decay_t<Data>, decay_t<Child>...> that has been
+            direct-list-initialized with the forwarded arguments, where
             `basic-sender` is the following exposition-only
             class template except as noted below:
 
@@ -6589,7 +6616,7 @@ namespace std::execution {
                   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>
-                concept well-formed = requires { typename T<Args...>; }; // exposition only
+                concept valid-specialization = requires { typename T<Args...>; }; // exposition only
 
                 struct default-impls {  // exposition only
                   static constexpr auto get-attrs = see below;
@@ -6609,16 +6636,13 @@ namespace std::execution {
                 template<class Index, class Sndr, class Rcvr> // exposition only
                 using env-type = call-result-t<
                   decltype(impls-for<tag_of_t<Sndr>>::get-env), Index,
-                  state-type<Sndr, Rcvr>&, const Rcvr&>>;
+                  state-type<Sndr, Rcvr>&, const Rcvr&>;
 
-                template<class Sndr>
-                using data-type = decltype((declval<Sndr>().data)); // exposition only
-
-                template<class Sndr, size_t N = 0>
-                using child-type = decltype((declval<Sndr>().childN)); // exposition only
+                template<class Sndr, size_t I = 0>
+                using child-type = decltype(declval<Sndr>().template get<I+2>()); // exposition only
 
                 template<class Sndr>
-                using indices-for = typename remove_reference_t<Sndr>::indices-for; // exposition only
+                using indices-for = remove_reference_t<Sndr>::indices-for; // exposition only
 
                 template<class Sndr, class Rcvr>
                 struct basic-state { // exposition only
@@ -6631,7 +6655,7 @@ namespace std::execution {
                 };
 
                 template<class Sndr, class Rcvr, class Index>
-                  requires well-formed<env-type, Index, Sndr, Rcvr>
+                  requires valid-specialization<env-type, Index, Sndr, Rcvr>
                 struct basic-receiver {  // exposition only
                   using receiver_concept = receiver_t;
 
@@ -6670,8 +6694,8 @@ namespace std::execution {
                   decltype(connect-all), basic-state<Sndr, Rcvr>*, Sndr, indices-for<Sndr>>;
 
                 template<class Sndr, class Rcvr>
-                  requires well-formed<state-type, Sndr, Rcvr> &&
-                           well-formed<connect-all-result, Sndr, Rcvr>
+                  requires valid-specialization<state-type, Sndr, Rcvr> &&
+                           valid-specialization<connect-all-result, Sndr, Rcvr>
                 struct basic-operation : basic-state<Sndr, Rcvr> {  // exposition only
                   using operation_state_concept = operation_state_t;
                   using tag-t = tag_of_t<Sndr>; // exposition only
@@ -6690,15 +6714,16 @@ namespace std::execution {
                 };
 
                 template<class Sndr, class Env>
-                using completion-signatures-for =  see below; // exposition only
+                using completion-signatures-for = see below; // exposition only
 
                 template<class Tag, class Data, class... Child>
-                struct basic-sender {  // exposition only
+                struct basic-sender : product-type<Tag, Data, Child...> {  // exposition only
                   using sender_concept = sender_t;
                   using indices-for = index_sequence_for<Child...>; // exposition only
 
                   decltype(auto) get_env() const noexcept {
-                    return impls-for<Tag>::get-attrs(data, child0, ... childn-1);
+                    auto& [_, data, ...child] = *this;
+                    return impls-for<Tag>::get-attrs(data, child...);
                   }
 
                   template<decays-to<basic-sender> Self, receiver Rcvr>
@@ -6712,26 +6737,19 @@ namespace std::execution {
                     -> completion-signatures-for<Self, Env> {
                     return {};
                   }
-
-                  Tag tag;            // exposition only
-                  Data data;          // exposition only
-                  Child0 child0;      // exposition only
-                  Child1 child1;      // exposition only
-                    ...
-                  Childn-1 childn-1;   // exposition only
                 };
               }
               
1. *Remarks:* The default template argument for the `Data` template parameter - denotes an unspecified empty trivial class type. + denotes an unspecified empty trivially copyable class type that models + `semiregular`. - 2. It is unspecified whether instances of `basic-sender` can be - aggregate initialized. + 2. It is unspecified whether a specialization of `basic-sender` is + an aggregate. - 3. An expression of type - `basic-sender` is usable as the initializer of a - structured binding declaration [dcl.struct.bind]. + 3. An expression of type `basic-sender` is usable as the initializer of a + structured binding declaration [dcl.struct.bind]. 4. The expression in the `noexcept` clause of the constructor of `basic-state` is: @@ -6741,30 +6759,30 @@ namespace std::execution { nothrow-callable<decltype(impls-for<tag_of_t<Sndr>>::get-state), Sndr, Rcvr&> - 5. The object `connect-all` is initialized with the following - lambda: + 5. The object `connect-all` is initialized with a callable object + equivalent to the following lambda:
               []<class Sndr, class Rcvr, size_t... Is>(
-                basic-state<Sndr, Rcvr>* op, Sndr&& sndr, index_sequence<Is...>) noexcept(noexcept(E))
-                  -> decltype(E) {
-                  return E;
+                basic-state<Sndr, Rcvr>* op, Sndr&& sndr, index_sequence<Is...>) noexcept(see below)
+                  -> decltype(auto) {
+                  auto& [_, data, ...child] = sndr;
+                  return product-type{connect(
+                    std::forward_like<Sndr>(child),
+                    basic-receiver<Sndr, Rcvr, integral_constant<size_t, Is>>{op})...};
                 }
               
- where `E` is the following expression: + 1. *Requires:* The expression in the `return` statement is well-formed. -
-              product-type{connect(
-                std::forward<Sndr>(sndr).childIs,
-                basic-receiver<Sndr, Rcvr, integral_constant<size_t, Is>>{op})...}
-              
+ 2. *Remarks:* The expression in the `noexcept` clause is `true` if + the `return` statement is not potentially throwing; otherwise, `false`. 6. The expression in the `noexcept` clause of the constructor of `basic-operation` is:
-            is_nothrow_constructible_v<basic-state<Self, Rcvr>, Self, Rcvr> &&
+            is_nothrow_constructible_v<basic-state<Self, Rcvr>, Self, Rcvr> &&
             noexcept(connect-all(this, std::forward<Sndr>(sndr), indices-for<Sndr>()))
             
@@ -6772,7 +6790,7 @@ namespace std::execution { `basic-sender` is:
-            is_nothrow_constructible_v<basic-operation<Self, Rcvr>, Self, Rcvr>
+            is_nothrow_constructible_v<basic-operation<Self, Rcvr>, Self, Rcvr>
             
8. The member default-impls::get-attrs is @@ -6780,7 +6798,7 @@ namespace std::execution { lambda:
-              [](const auto& data, const auto&... child) noexcept -> decltype(auto) {
+              [](const auto&, const auto&... child) noexcept -> decltype(auto) {
                 if constexpr (sizeof...(child) == 1)
                   return (FWD-ENV(get_env(child)), ...);
                 else
@@ -6792,7 +6810,7 @@ namespace std::execution {
             with a callable object equivalent to the following lambda:
 
               
-              []<class Rcvr>(auto index, auto& state, const Rcvr& rcvr) noexcept -> decltype(auto) {
+              [](auto, auto&, const auto& rcvr) noexcept -> decltype(auto) {
                 return FWD-ENV(get_env(rcvr));
               }
               
@@ -6802,7 +6820,8 @@ namespace std::execution {
               []<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept -> decltype(auto) {
-                return (std::forward<Sndr>(sndr).data);
+                auto& [_, data, ...child] = sndr;
+                return std::forward_like<Sndr>(data);
               }
               
@@ -6810,7 +6829,7 @@ namespace std::execution { with a callable object equivalent to the following lambda:
-              [](auto& state, auto& rcvr, auto&... ops) noexcept -> void {
+              [](auto&, auto&, auto&... ops) noexcept -> void {
                 (execution::start(ops), ...);
               }
               
@@ -6871,12 +6890,12 @@ namespace std::execution { template<class Sndr> concept sender = - bool(enable-sender<remove_cvref_t<Sndr>>) && // atomic constraint + bool(enable-sender<remove_cvref_t<Sndr>>) && // atomic constraint ([temp.constr.atomic]) requires (const remove_cvref_t<Sndr>& sndr) { { get_env(sndr) } -> queryable; } && - move_constructible<remove_cvref_t<Sndr>> && // rvalues are movable, and - constructible_from<remove_cvref_t<Sndr>, Sndr>; // lvalues are copyable + move_constructible<remove_cvref_t<Sndr>> && // senders are movable and + constructible_from<remove_cvref_t<Sndr>, Sndr>; // decay copyable template<class Sndr, class Env = empty_env> concept sender_in = @@ -6897,9 +6916,10 @@ namespace std::execution { }
-2. Given a subexpression `sndr`, let `Sndr` be `decltype((sndr))`, let `Env` be - the type of an environment, and let `rcvr` be a receiver with an associated - environment `Env`. A completion operation is a permissible completion for `Sndr` and `Env` if its completion signature appears in the argument list of the specialization of `completion_signatures` denoted by `completion_signatures_of_t`. @@ -6908,12 +6928,12 @@ namespace std::execution { starting the resulting operation state are permissible completions for `Sndr` and `Env`. -3. A type `Sigs` satisfies and models the exposition-only concept +3. A type models the exposition-only concept `valid-completion-signatures` if it denotes a specialization of the `completion_signatures` class template. 4. The exposition-only concepts `sender-of` and - `sender-of-in` define the requirements for a sender + `sender-in-of` define the requirements for a sender type that completes with a given unique set of value result types.
@@ -6922,14 +6942,14 @@ namespace std::execution {
         using value-signature = set_value_t(As...); // exposition only
 
       template<class Sndr, class Env, class... Values>
-        concept sender-of-in =
+        concept sender-in-of =
           sender_in<Sndr, Env> &&
           MATCHING-SIG( // see [exec.general]
             set_value_t(Values...),
             value_types_of_t<Sndr, Env, value-signature, type_identity_t>);
 
       template<class Sndr, class... Values>
-        concept sender-of = sender-of-in<Sndr, empty_env, Values...>;
+        concept sender-of = sender-in-of<Sndr, empty_env, Values...>;
     }
     
@@ -6971,20 +6991,18 @@ namespace std::execution { - Only expose an overload of a member `connect` that accepts an lvalue sender if they model `copy_constructible`. - - Model `copy_constructible` if they satisfy `copy_constructible`. - ### Awaitable helpers [exec.awaitables] ### {#spec.exec-awaitables} -1. The sender concepts recognize awaitables as senders. For this clause - ([exec]), an awaitable is an expression that would be +1. The sender concepts recognize awaitables as senders. For [exec], an + awaitable is an expression that would be well-formed as the operand of a `co_await` expression within a given context. 2. For a subexpression `c`, let GET-AWAITER(c, p) be expression-equivalent to the series of transformations and conversions applied to `c` as the operand of an *await-expression* in a coroutine, - resulting in lvalue `e` as described by [expr.await]/3.2-4, where `p` - is an lvalue referring to the coroutine's promise type, `Promise`. `e` as described by [expr.await], where `p` + is an lvalue referring to the coroutine's promise, which has type `Promise`. This includes the invocation of the promise type's `await_transform` member if any, the invocation of the `operator co_await` picked by overload resolution if any, and any necessary implicit @@ -7000,7 +7018,7 @@ namespace std::execution {
     namespace std {
       template<class T>
-      concept await-suspend-result = see below;
+      concept await-suspend-result = see below; // exposition only
 
       template<class A, class Promise>
       concept is-awaiter = // exposition only
@@ -7047,9 +7065,8 @@ namespace std::execution {
             }
 
           template<has-as-awaitable<Derived> T>
-            auto await_transform(T&& value)
-              noexcept(noexcept(std::forward<T>(value).as_awaitable(declval<Derived&>())))
-              -> decltype(std::forward<T>(value).as_awaitable(declval<Derived&>())) {
+            decltype(auto) await_transform(T&& value)
+              noexcept(noexcept(std::forward<T>(value).as_awaitable(declval<Derived&>()))) {
               return std::forward<T>(value).as_awaitable(static_cast<Derived&>(*this));
             }
         };
@@ -7217,12 +7234,14 @@ namespace std::execution {
 1. `get_completion_signatures` is a customization point object. Let `sndr` be an
     expression such that `decltype((sndr))` is `Sndr`, and let `env` be an
     expression such that `decltype((env))` is `Env`. Then
-    `get_completion_signatures(sndr, env)` is expression-equivalent to:
+    `get_completion_signatures(sndr, env)` is expression-equivalent to
+    (void(sndr), void(env), CS()) except that `void(sndr)`
+    and `void(env)` are indeterminately sequenced, where `CS` is:
 
-    1. `decltype(sndr.get_completion_signatures(env)){}` if that
-        expression is well-formed,
+    1. `decltype(sndr.get_completion_signatures(env))` if that
+        type is well-formed,
 
-    2. Otherwise, `remove_cvref_t::completion_signatures{}` if that expression is well-formed,
+    2. Otherwise, `remove_cvref_t::completion_signatures` if that type is well-formed,
 
     3. Otherwise, if is-awaitable<Sndr, env-promise<Env>>
         is `true`, then:
@@ -7232,13 +7251,14 @@ namespace std::execution {
               SET-VALUE-SIG(await-result-type<Sndr,
                             env-promise<Env>>), // see [exec.snd.concepts]
               set_error_t(exception_ptr),
-              set_stopped_t()>{}
+              set_stopped_t()>
             
- 4. Otherwise, `get_completion_signatures(sndr, env)` is ill-formed. + 4. Otherwise, `CS` is ill-formed. -2. Let `rcvr` be an rvalue receiver of type `Rcvr`, and let `Sndr` be the type of a - sender such that `sender_in>` is `true`. Let `Sigs...` be the +2. Let `rcvr` be an rvalue whose type `Rcvr` models `receiver`, and let `Sndr` + be the type of a sender such that `sender_in>` is + `true`. Let `Sigs...` be the template arguments of the `completion_signatures` specialization named by `completion_signatures_of_t>`. Let `CSO` be a completion function. If sender `Sndr` or its operation state cause the @@ -7254,16 +7274,15 @@ namespace std::execution { 2. The name `connect` denotes a customization point object. For subexpressions `sndr` and `rcvr`, let `Sndr` be `decltype((sndr))` and `Rcvr` be - `decltype((rcvr))`, and let `DS` and `DR` be the decayed types of `Sndr` and - `Rcvr`, respectively. + `decltype((rcvr))`, and let `DS` and `DR` be `decay_t` and + `decay_t`, respectively. -3. Let `connect-awaitable-promise` be the following class: +3. Let `connect-awaitable-promise` be the following exposition-only class:
     namespace std::execution {
       struct connect-awaitable-promise
         : with-await-transform<connect-awaitable-promise> {
-        DR& rcvr; // exposition only
 
         connect-awaitable-promise(DS&, DR& rcvr) noexcept : rcvr(rcvr) {}
 
@@ -7273,7 +7292,7 @@ namespace std::execution {
         [[noreturn]] void return_void() noexcept { terminate(); }
 
         coroutine_handle<> unhandled_stopped() noexcept {
-          set_stopped((DR&&) rcvr);
+          set_stopped(std::move(rcvr));
           return noop_coroutine();
         }
 
@@ -7282,21 +7301,23 @@ namespace std::execution {
             coroutine_handle<connect-awaitable-promise>::from_promise(*this)};
         }
 
-        env_of_t<const DR&> get_env() const noexcept {
+        env_of_t<DR> get_env() const noexcept {
           return execution::get_env(rcvr);
         }
+
+      private:
+        DR& rcvr; // exposition only
       };
     }
     
-4. Let `operation-state-task` be the following class: +4. Let `operation-state-task` be the following exposition-only class:
     namespace std::execution {
       struct operation-state-task {
         using operation_state_concept = operation_state_t;
         using promise_type = connect-awaitable-promise;
-        coroutine_handle<> coro; // exposition only
 
         explicit operation-state-task(coroutine_handle<> h) noexcept : coro(h) {}
         operation-state-task(operation-state-task&& o) noexcept
@@ -7306,6 +7327,9 @@ namespace std::execution {
         void start() & noexcept {
           coro.resume();
         }
+
+      private:
+        coroutine_handle<> coro; // exposition only
       };
     }
     
@@ -7337,7 +7361,7 @@ namespace std::execution { [[noreturn]] void await_resume() noexcept { unreachable(); } }; return awaiter{fn}; - }; + } operation-state-task connect-awaitable(DS sndr, DR rcvr) requires receiver_of<DR, Sigs> { exception_ptr ep; @@ -7356,8 +7380,7 @@ namespace std::execution { } -6. If `Sndr` does not satisfy `sender` or if `Rcvr` does not satisfy `receiver`, - `connect(sndr, rcvr)` is ill-formed. Otherwise, the expression `connect(sndr, rcvr)` is +6. The expression `connect(sndr, rcvr)` is expression-equivalent to: 1. `sndr.connect(rcvr)` if that expression is well-formed. @@ -7365,27 +7388,27 @@ namespace std::execution { * Mandates: The type of the expression above satisfies `operation_state`. - 2. Otherwise, connect-awaitable(sndr, rcvr) if that expression is - well-formed. + 2. Otherwise, connect-awaitable(sndr, rcvr). + + 3. *Mandates:* `sender && receiver` is `true`. - 3. Otherwise, `connect(sndr, rcvr)` is ill-formed. ### Sender factories [exec.factories] ### {#spec-execution.senders.factories} #### `execution::schedule` [exec.schedule] #### {#spec-execution.senders.schedule} -1. `schedule` obtains a schedule-sender ([async.ops]) from a scheduler. +1. `schedule` obtains a schedule sender ([async.ops]) from a scheduler. 2. The name `schedule` denotes a customization point object. For a - subexpression `sch`, the expression `schedule(sch)` is expression-equivalent to: + subexpression `sch`, the expression `schedule(sch)` is expression-equivalent to + `sch.schedule()`. + - 1. `sch.schedule()` if that expression is valid. If `sch.schedule()` does - not return a sender whose `set_value` completion scheduler is equal - to `sch`, the behavior of calling `schedule(sch)` is undefined. + 1. If the expression get_completion_scheduler<set_value_t>( + get_env(sch.schedule())) == sch is ill-formed or evaluates + to `false`, the behavior of calling `schedule(sch)` is undefined. - * Mandates: The type of `sch.schedule()` satisfies `sender`. - - 2. Otherwise, `schedule(sch)` is ill-formed. + 2. Mandates: The type of `sch.schedule()` satisfies `sender`. #### `execution::just`, `execution::just_error`, `execution::just_stopped` [exec.just] #### {#spec-execution.senders.just} @@ -7409,7 +7432,7 @@ namespace std::execution { is `false`; Otherwise, it is expression-equivalent to make-sender(just-cpo, - product-type{vs...}). + product-type{ts...}). 3. For `just`, `just_error`, and `just_stopped`, let `set-cpo` be `set_value`, `set_error`, and `set_stopped` respectively. The @@ -7459,33 +7482,34 @@ namespace std::execution { #### General [exec.adapt.general] #### {#spec-execution.senders.adapt.general} -1. Subclause [exec.adapt] specifies a set of sender adaptors. +1. [exec.adapt] specifies a set of sender adaptors. -2. The bitwise OR operator is overloaded for the purpose of creating sender +2. The bitwise inclusive OR operator is overloaded for the purpose of creating sender chains. The adaptors also support function call syntax with equivalent semantics. -3. Unless otherwise specified, a sender adaptor is prohibited from causing - observable effects, apart from moving and copying its arguments, before the - returned sender is connected with a receiver using `connect`, and `start` is - called on the resulting operation state. This requirement applies to any - function that is selected by the implementation of the sender adaptor. - -4. Unless otherwise specified, a parent sender ([async.ops]) with a single child - sender `sndr` has an associated attribute object equal to - FWD-ENV(get_env(sndr)) ([exec.fwd.env]). Unless - otherwise specified, a parent sender with more than one child senders has an - associated attributes object equal to empty_env{}. These - requirements apply to any function that is selected by the implementation of - the sender adaptor. - -5. Unless otherwise specified, when a parent sender is connected to a receiver - `rcvr`, any receiver used to connect a child sender has an associated - environment equal to FWD-ENV(get_env(rcvr)). This - requirement applies to any sender returned from a function that is selected - by the implementation of such sender adaptor. - -6. If a sender returned from a sender adaptor specified in this subclause is +3. Unless otherwise specified: + + 1. A sender adaptor is prohibited from causing observable effects, apart + from moving and copying its arguments, before the returned sender is + connected with a receiver using `connect`, and `start` is called on the + resulting operation state. + + 2. A parent sender ([async.ops]) with a single child + sender `sndr` has an associated attribute object equal to + FWD-ENV(get_env(sndr)) ([exec.fwd.env]). + + 3. A parent sender with more than one child sender has an + associated attributes object equal to empty_env{}. + + 4. When a parent sender is connected to a receiver `rcvr`, any receiver used + to connect a child sender has an associated environment equal to + FWD-ENV(get_env(rcvr)). + + These requirement apply to any function that is selected by the implementation + of the sender adaptor. + +4. If a sender returned from a sender adaptor specified in [exec.adapt] is specified to include `set_error_t(Err)` among its set of completion signatures where `decay_t` denotes the type `exception_ptr`, but the implementation does not potentially evaluate an error completion operation with an @@ -7495,7 +7519,7 @@ namespace std::execution { #### Sender adaptor closure objects [exec.adapt.objects] #### {#spec-execution.senders.adaptor.objects} 1. A pipeable sender adaptor closure object is a function object that - accepts one or more `sender` arguments and returns a `sender`. For a sender + accepts one or more `sender` arguments and returns a `sender`. For a pipeable sender adaptor closure object `c` and an expression `sndr` such that `decltype((sndr))` models `sender`, the following expressions are equivalent and yield a `sender`: @@ -7512,22 +7536,22 @@ namespace std::execution { `e` is a perfect forwarding call wrapper ([func.require]) with the following properties: - - Its target object is an object `d2` of type `decay_t` + - Its target object is an object `d2` of type `decltype(auto(d))` direct-non-list-initialized with `d`. - It has one bound argument entity, an object `c2` of type - `decay_t` direct-non-list-initialized with `C`. + `decltype(auto(c))` direct-non-list-initialized with `c`. - Its call pattern is `d2(c2(arg))`, where `arg` is the argument used in a function call expression of `e`. The expression `c | d` is well-formed if and only if the initializations of - the state entities of `e` are all well-formed. + the state entities ([func.def]) of `e` are all well-formed. 2. An object `t` of type `T` is a pipeable sender adaptor closure object if `T` models `derived_from>`, `T` has no other base classes of type `sender_adaptor_closure` for any other type `U`, and `T` - does not model `sender`. + does not satisfy `sender`. 3. The template parameter `D` for `sender_adaptor_closure` can be an incomplete type. Before any expression of type cv D appears as an @@ -7546,11 +7570,10 @@ namespace std::execution { 6. If a pipeable sender adaptor object `adaptor` accepts more than one argument, then let `sndr` be an expression such that `decltype((sndr))` models `sender`, let `args...` be arguments such that `adaptor(sndr, args...)` is a - well-formed expression as specified in the rest of this subclause - ([exec.adapt.objects]), and let `BoundArgs` be a pack that denotes - `decay_t...`. The expression `adaptor(args...)` produces a - pipeable sender adaptor closure object `f` that is a perfect forwarding call - wrapper with the following properties: + well-formed expression as specified below, and let `BoundArgs` be a pack + that denotes `decltype(auto(args))...`. The expression `adaptor(args...)` + produces a pipeable sender adaptor closure object `f` that is a perfect + forwarding call wrapper with the following properties: - Its target object is a copy of `adaptor`. @@ -7570,7 +7593,7 @@ namespace std::execution { 1. `on` adapts an input sender into a sender that will start on an execution agent belonging to a particular scheduler's associated execution resource. -2. The name `on` denotes a customization point object. For some subexpressions +2. The name `on` denotes a customization point object. For subexpressions `sch` and `sndr`, if `decltype((sch))` does not satisfy `scheduler`, or `decltype((sndr))` does not satisfy `sender`, `on(sch, sndr)` is ill-formed. @@ -7579,9 +7602,11 @@ namespace std::execution {
         transform_sender(
           query-or-default(get_domain, sch, default_domain()),
-          make-sender(on, sch, sndr));
+          make-sender(on, sch, sndr))
         
+ except that `sch` is evaluated only once. + 4. Let `out_sndr` and `env` be subexpressions such that `OutSndr` is `decltype((out_sndr))`. If sender-for<OutSndr, on_t> is `false`, then the expressions `on.transform_env(out_sndr, env)` and `on.transform_sender(out_sndr, env)` are ill-formed; @@ -7590,17 +7615,18 @@ namespace std::execution { - `on.transform_env(out_sndr, env)` is equivalent to:
-            auto&& [ign1, sch, ign2] = out_sndr;
+            auto&& [_, sch, _] = out_sndr;
             return JOIN-ENV(SCHED-ENV(sch), FWD-ENV(env));
             
- `on.transform_sender(out_sndr, env)` is equivalent to:
-            auto&& [ign, sch, sndr] = out_sndr;
+            auto&& [_, sch, sndr] = out_sndr;
             return let_value(
               schedule(sch),
-              [sndr = std::forward_like<OutSndr>(sndr)]() mutable {
+              [sndr = std::forward_like<OutSndr>(sndr)]() mutable
+                noexcept(is_nothrow_move_constructible_v) {
                 return std::move(sndr);
               });
             
@@ -7611,16 +7637,14 @@ namespace std::execution { type `Env` such that `sender_in` is `true`. Let `op` be an lvalue referring to the operation state that results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)` shall start `sndr` on an execution agent of the - associated execution resource of `sch`, or failing that, shall execute an - error completion on `out_rcvr`. + associated execution resource of `sch`. If scheduling onto `sch` fails, an error + completion on `out_rcvr` shall be executed on an unspecified execution agent. #### `execution::transfer` [exec.transfer] #### {#spec-execution.senders.adapt.transfer} -1. `transfer` adapts a sender into one with a different associated `set_value` - completion scheduler. It results in a transition - between different execution resources when executed. +1. `transfer` adapts a sender into one that completes on the specified scheduler. -2. The name `transfer` denotes a customization point object. For some +2. The name `transfer` denotes a customization point object. For subexpressions `sch` and `sndr`, if `decltype((sch))` does not satisfy `scheduler`, or `decltype((sndr))` does not satisfy `sender`, `transfer(sndr, sch)` is ill-formed. @@ -7630,9 +7654,11 @@ namespace std::execution {
         transform_sender(
           get-domain-early(sndr),
-          make-sender(transfer, sch, sndr));
+          make-sender(transfer, sch, sndr))
         
+ except that `sndr` is evaluated only once. + 4. The exposition-only class template `impls-for` is specialized for `transfer_t` as follows: @@ -7654,13 +7680,13 @@ namespace std::execution { is equal to:
-        auto [tag, data, child] = sndr;
+        auto [_, data, child] = sndr;
         return schedule_from(std::move(data), std::move(child));
         
This causes the `transfer(sndr, sch)` sender to become - `schedule_from(sch, sndr)` when it is connected with a receiver with an - execution domain that does not customize `transfer`. + `schedule_from(sch, sndr)` when it is connected with a receiver whose + execution domain does not customize `transfer`.
6. Let `out_sndr` be a subexpression denoting a sender returned from `transfer(sndr, sch)` or one equal to such, and let `OutSndr` be the type @@ -7670,8 +7696,8 @@ namespace std::execution { results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)` shall start `sndr` on the current execution agent and execute completion operations on `out_rcvr` on an execution agent of the execution resource - associated with `sch`. If scheduling onto `sch` fails, execute an error - completion on `out_rcvr` on an unspecified execution agent. + associated with `sch`. If scheduling onto `sch` fails, an error completion + on `out_rcvr` shall be executed on an unspecified execution agent. #### `execution::schedule_from` [exec.schedule.from] #### {#spec-execution.senders.adaptors.schedule_from} @@ -7683,7 +7709,7 @@ namespace std::execution { 2. The name `schedule_from` denotes a customization point object. For some subexpressions `sch` and `sndr`, let `Sch` be `decltype((sch))` and `Sndr` be `decltype((sndr))`. If `Sch` does not satisfy `scheduler`, or `Sndr` does not - satisfy `sender`, `schedule_from` is ill-formed. + satisfy `sender`, `schedule_from(sch, sndr)` is ill-formed. 3. Otherwise, the expression `schedule_from(sch, sndr)` is expression-equivalent to: @@ -7691,9 +7717,11 @@ namespace std::execution {
         transform_sender(
           query-or-default(get_domain, sch, default_domain()),
-          make-sender(schedule_from, sch, sndr));
+          make-sender(schedule_from, sch, sndr))
         
+ except that `sch` is evaluated only once. + 4. The exposition-only class template `impls-for` ([exec.snd.general]) is specialized for `schedule_from_t` as follows: @@ -7722,30 +7750,32 @@ namespace std::execution { with a callable object equivalent to the following lambda:
-          []<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr)
+          []<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(see below)
               requires sender_in<child-type<Sndr>, env_of_t<Rcvr>> {
-            return apply(
-              [&]<class Sch, class Child>(auto, Sch sch, Child&& child) {
-                using variant-type = see below;
-                using receiver-type = see below;
-                using operation-type = connect_result_t<schedule_result_t<Sch>, receiver-type>;
-
-                struct state-type {
-                  Rcvr& rcvr;
-                  variant-type async-result;
-                  operation-type op-state;
-
-                  explicit state-type(Sch sch, Rcvr& rcvr)
-                    : rcvr(rcvr), op-state(connect(schedule(sch), receiver-type{{}, this})) {}
-                };
 
-                return state-type{sch, rcvr};
-              },
-              std::forward<Sndr>(sndr));
+            auto& [_, sch, child] = sndr;
+
+            using sched-t = decltype(auto(sch));
+            using variant-type = see below;
+            using receiver-type = see below;
+            using operation-type = connect_result_t<schedule_result_t<sched-t>, receiver-type>;
+            constexpr bool nothrow = noexcept(connect(schedule(sch), receiver-type{nullptr}));
+
+            struct state-type {
+              Rcvr& rcvr;
+              variant-type async-result;
+              operation-type op-state;
+
+              explicit state-type(sched-t sch, Rcvr& rcvr) noexcept(nothrow)
+                : rcvr(rcvr), op-state(connect(schedule(sch), receiver-type{this})) {}
+            };
+
+            return state-type{sch, rcvr};
           }
           
- 1. The local class `state-type` is a structural type. + 1. Objects of the local class `state-type` can be used to + initialize a structured binding. 2. Let `Sigs` be a pack of the arguments to the `completion_signatures` specialization named by @@ -7766,9 +7796,6 @@ namespace std::execution { using receiver_concept = receiver_t; state-type* state; // exposition only - Rcvr&& base() && noexcept { return std::move(state->rcvr); } - const Rcvr& base() const & noexcept { return state->rcvr; } - void set_value() && noexcept { visit( [this]<class Tuple>(Tuple& result) noexcept -> void { @@ -7796,6 +7823,10 @@ namespace std::execution { } + 4. The expression in the `noexcept` clause of the lambda is `true` if + the construction of the returned `state-type` object is not + potentially throwing; otherwise, `false`. + 3. The member impls-for<schedule_from_t>::complete is initialized with a callable object equivalent to the following lambda: @@ -7817,18 +7848,16 @@ namespace std::execution { }; -5. Let the subexpression `out_sndr` denote the result of the invocation - `schedule_from(sch, sndr)` or an object copied or moved from such, and let - the subexpression `rcvr` denote a receiver such that the expression - `connect(out_sndr, rcvr)` is well-formed. The expression - `connect(out_sndr, rcvr)` has undefined behavior unless it creates an - asynchronous operation ([async.ops]) that, when started: - - - eventually completes on an execution agent belonging to the associated - execution resource of `sch`, and - - - completes with the same async result as `sndr`. - +5. Let `out_sndr` be a subexpression denoting a sender returned from + `schedule_from(sch, sndr)` or one equal to such, and let `OutSndr` be the type + `decltype((out_sndr))`. Let `out_rcvr` be a subexpression denoting a + receiver that has an environment of type `Env` such that `sender_in` is `true`. Let `op` be an lvalue referring to the operation state that + results from connecting `out_sndr` with `out_rcvr`. Calling `start(op)` + shall start `sndr` on the current execution agent and execute completion + operations on `out_rcvr` on an execution agent of the execution resource + associated with `sch`. If scheduling onto `sch` fails, an error completion + on `out_rcvr` shall be executed on an unspecified execution agent. #### `execution::then`, `execution::upon_error`, `execution::upon_stopped` [exec.then] #### {#spec-execution.senders.adaptor.then} @@ -7839,9 +7868,9 @@ namespace std::execution { 2. The names `then`, `upon_error`, and `upon_stopped` denote customization point objects. Let the expression `then-cpo` be one of `then`, - `upon_error`, or `upon_stopped`. For subexpressions `sndr` and `f`, let `Sndr` be - `decltype((sndr))` and let `F` be the decayed type of `f`. If `Sndr` does not - satisfy `sender`, or `F` does not satisfy `movable-value`, + `upon_error`, or `upon_stopped`. For subexpressions `sndr` and `f`, if + `decltype(sndr)` does not satisfy `sender`, or `decltype(f)` does not + satisfy `movable-value`, then-cpo(sndr, f) is ill-formed. 3. Otherwise, the expression then-cpo(sndr, f) is @@ -7850,8 +7879,10 @@ namespace std::execution {
         transform_sender(
           get-domain-early(sndr),
-          make-sender(then-cpo, f, sndr));
+          make-sender(then-cpo, f, sndr))
         
+ + except that `sndr` is evaluated only once. 4. For `then`, `upon_error`, and `upon_stopped`, let `set-cpo` be `set_value`, `set_error`, and `set_stopped` respectively. The @@ -7864,7 +7895,7 @@ namespace std::execution { struct impls-for<decayed-typeof<then-cpo>> : default-impls { static constexpr auto complete = []<class Tag, class... Args> - (auto /*index*/, auto& fn, auto& rcvr, Tag, Args&&... args) noexcept -> void { + (auto, auto& fn, auto& rcvr, Tag, Args&&... args) noexcept -> void { if constexpr (same_as<Tag, decayed-typeof<set-cpo>>) { TRY-SET-VALUE(std::move(rcvr), invoke(std::move(fn), std::forward<Args>(args)...)); @@ -7880,8 +7911,8 @@ namespace std::execution { unless it returns a sender `out_sndr` that: 1. Invokes `f` or a copy of such with the value, error, or stopped result - datums of `sndr` (for `then`, `upon_error`, and `upon_stopped` - respectively), using the result value of `f` as `out_sndr`'s value + datums of `sndr` for `then`, `upon_error`, and `upon_stopped` + respectively, using the result value of `f` as `out_sndr`'s value completion, and 2. Forwards all other completion operations unchanged. @@ -7893,10 +7924,10 @@ namespace std::execution { operation by passing the sender's result datums to a user-specified callable, which returns a new sender that is connected and started. -2. Let the expression `let-cpo` be one of `let_value`, - `let_error`, or `let_stopped` and let `set-cpo` be the - completion function that corresponds to `let-cpo` - (`set_value` for `let_value`, etc.). For a subexpression `sndr`, let +2. For `let_value`, `let_error`, and `let_stopped`, let `set-cpo` + be `set_value`, `set_error`, and `set_stopped` respectively. + Let the expression `let-cpo` be one of `let_value`, + `let_error`, or `let_stopped`. For a subexpression `sndr`, let let-env(sndr) be expression-equivalent to the first well-formed expression below: @@ -7904,12 +7935,12 @@ namespace std::execution { - MAKE-ENV(get_domain, get_domain(get_env(sndr))) - - `empty_env{}` + - `(void(sndr), empty_env{})` 3. The names `let_value`, `let_error`, and `let_stopped` denote customization - point objects. For subexpressions `sndr` and `f`, let `Sndr` be `decltype((sndr))`, - let `F` be the decayed type of `f`. If `Sndr` does not satisfy `sender` or if `F` - does not satisfy `movable-value`, the expression + point objects. For subexpressions `sndr` and `f`, let `F` be the decayed + type of `f`. If `decltype(sndr)` does not satisfy `sender` or if + `decltype(f)` does not satisfy `movable-value`, the expression let-cpo(sndr, f) is ill-formed. If `F` does not satisfy `invocable`, the expression `let_stopped(sndr, f)` is ill-formed. @@ -7919,9 +7950,11 @@ namespace std::execution {
       transform_sender(
         get-domain-early(sndr),
-        make-sender(let-cpo, f, sndr));
+        make-sender(let-cpo, f, sndr))
       
+ except that `sndr` is evaluated only once. + 5. The exposition-only class template `impls-for` ([exec.snd.general]) is specialized for `let-cpo` as follows: @@ -7944,38 +7977,51 @@ namespace std::execution {
             namespace std::execution {
               template<class Rcvr, class Env>
-              struct receiver2 : Rcvr {
-                explicit receiver2(Rcvr rcvr, Env env)
-                  : Rcvr(std::move(rcvr)), env(std::move(env)) {}
+              struct receiver2 {
+                using receiver_concept = receiver_t;
+
+                template<class... Args>
+                void set_value(Args&&... args) && noexcept {
+                  execution::set_value(std::move(rcvr), std::forward<Args>(args)...);
+                }
+
+                template<class Error>
+                void set_error(Error&& err) && noexcept {
+                  execution::set_error(std::move(rcvr), std::forward<Error>(err));
+                }
+
+                void set_stopped() && noexcept {
+                  execution::set_stopped(std::move(rcvr));
+                }
 
-                auto get_env() const noexcept {
-                  const Rcvr& rcvr = *this;
+                decltype(auto) get_env() const noexcept {
                   return JOIN-ENV(env, FWD-ENV(execution::get_env(rcvr)));
                 }
 
+                Rcvr& rcvr; // exposition only
                 Env env; // exposition only
               };
             }
             
2. impls-for<decayed-typeof<let-cpo>>::get-state is - is initialized with a callable object equivalent to the following: + initialized with a callable object equivalent to the following:
             []<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) requires see below {
-              auto&& [tag, data, child] = std::forward<Sndr>(sndr);
-              return [&]<class Fn, class Env>(Fn fn, Env env) {
-                using args-variant-type = see below;
-                using ops2-variant-type = see below;
-
-                struct state-type {
-                  Fn fn;
-                  Env env;
-                  args-variant-type args;
-                  ops2-variant-type ops2;
-                };
-                return state-type{std::move(fn), std::move(env), {}, {}};
-              }(std::forward_like<Sndr>(data), let-env(child));
+              auto& [_, fn, child] = sndr;
+              using fn-t = decay_t<decltype(fn)>;
+              using env-t = decltype(let-env(child));
+              using args-variant-type = see below;
+              using ops2-variant-type = see below;
+
+              struct state-type {
+                Fn fn;
+                Env env;
+                args-variant-type args;
+                ops2-variant-type ops2;
+              };
+              return state-type{std::forward_like<Sndr>(fn), let-env(child), {}, {}};
             }
             
@@ -7988,7 +8034,7 @@ namespace std::execution { as-tuple<Tag(Args...)> denotes the type decayed-tuple<Args...>. Then `args-variant-type` denotes the type variant<monostate, - as-tuple<LetSigs>...>. + as-tuple<LetSigs>...> except with duplicate types removed. 2. Let `as-sndr2` be an alias template such that as-sndr2<Tag(Args...)> denotes the type @@ -7996,21 +8042,22 @@ namespace std::execution { Then `ops2-variant-type` denotes the type variant<monostate, connect_result_t<as-sndr2<LetSigs>, - receiver2<Rcvr, Env>>...>. + receiver2<Rcvr, Env>>...> except with duplicate types removed. 3. The requires-clause constraining the above lambda is satisfied if and only if the types `args-variant-type` and `ops2-variant-type` are well-formed. - 3. The exposition-only function template `let-bind` is equal to: + 3. The exposition-only function template `let-bind` has effects equivalent to:
-            auto& args = state.args.emplace<decayed-tuple<Args...>>(std::forward<Args>(args)...);
-            auto sndr2 = apply(std::move(state.fn), args);
-            auto rcvr2 = receiver2{std::move(rcvr), std::move(state.env)};
-            auto mkop2 = [&] { return connect(std::move(sndr2), std::move(rcvr2)); };
-            auto& op2 = state.ops2.emplace<decltype(mkop2())>(emplace-from{mkop2});
-            start(op2);
+            using args_t = decayed-tuple<Args...>;
+            auto mkop2 = [&] {
+              return connect(
+                apply(std::move(state.fn), state.args.emplace<args_t>(std::forward<Args>(args)...)),
+                receiver2{rcvr, std::move(state.env)});
+            };
+            start(state.ops2.emplace<decltype(mkop2())>(emplace-from{mkop2}));
             
4. impls-for<decayed-typeof<let-cpo>>::complete is @@ -8034,7 +8081,7 @@ namespace std::execution { JOIN-ENV(let-env(sndr), FWD-ENV(env)). 7. Let the subexpression `out_sndr` denote the result of the invocation - let-cpo(sndr, f) or an object copied or moved from such, + let-cpo(sndr, f) or an object equal to such, and let the subexpression `rcvr` denote a receiver such that the expression `connect(out_sndr, rcvr)` is well-formed. The expression `connect(out_sndr, rcvr)` has undefined behavior unless it creates an asynchronous operation @@ -8053,21 +8100,22 @@ namespace std::execution { 1. `bulk` runs a task repeatedly for every index in an index space. 2. The name `bulk` denotes a customization point object. For subexpressions - `sndr`, `shape`, and `f`, let `Sndr` be `decltype((sndr))`, let `Shape` be - the decayed type of `shape`, and let `F` be the decayed type of `f`. If - `Sndr` does not satisfy `sender`, or if `Shape` does not satisfy `integral`, - or if `F` does not satisfy `movable-value`, bulk(sndr, - shape, f) is ill-formed. + `sndr`, `shape`, and `f`, let `Shape` be `decltype(auto(shape))`. If + `decltype((sndr))` does not satisfy `sender`, or if `Shape` does not + satisfy `integral`, or if `decltype((f))` does not satisfy `movable-value`, + `bulk(sndr, shape, f)` is ill-formed. -3. Otherwise, the expression bulk(sndr, shape, f) is +3. Otherwise, the expression `bulk(sndr, shape, f)` is expression-equivalent to:
         transform_sender(
           get-domain-early(sndr),
-          make-sender(bulk, product-type{shape, f}, sndr));
+          make-sender(bulk, product-type{shape, f}, sndr))
         
+ except that `sndr` is evaluated only once. + 4. The exposition-only class template `impls-for` ([exec.snd.general]) is specialized for `bulk_t` as follows: @@ -8090,7 +8138,7 @@ namespace std::execution { auto& [shape, f] = state; constexpr bool nothrow = noexcept(f(auto(shape), args...)); TRY-EVAL(std::move(rcvr), [&]() noexcept(nothrow) { - for (auto max = shape, i = 0; i < max; ++i) { + for (decltype(auto(shape)) i = 0; i < shape; ++i) { f(auto(i), args...); } Tag()(std::move(rcvr), std::forward<Args>(args)...); @@ -8106,7 +8154,7 @@ namespace std::execution { or if the expression `f(auto(shape), args...)` is well-formed. 5. Let the subexpression `out_sndr` denote the result of the invocation - bulk(sndr, shape, f) or an object copied or moved from such, + `bulk(sndr, shape, f)` or an object equal to such, and let the subexpression `rcvr` denote a receiver such that the expression `connect(out_sndr, rcvr)` is well-formed. The expression `connect(out_sndr, rcvr)` has undefined behavior unless it creates an asynchronous operation @@ -8119,6 +8167,8 @@ namespace std::execution { - propagates all completion operations sent by `sndr`. + + #### `execution::split` and `execution::ensure_started` [exec.split] #### {#spec-execution.senders.adapt.split} 1. `split` adapts an arbitrary sender into a sender that can be connected