---
title: "when_all Oughtn’t Hallucinate set_stopped"
document: P4269R0
date: 2026-06-23
audience: SG1, LEWG
reply-to:
  - "Robert Leahy <rleahy@rleahy.ca>"
---

When `std::execution::when_all` is provided with multiple senders it becomes possible that:

* A child operation will end unhappily, and
* Other operations will still be outstanding

In which scenario `std::execution::when_all` reacts by calling `std::inplace_stop_source::request_stop`. This means that a `std::inplace_stop_source` must be available for its use.

However one notes that the above scenario requires that a child operation end unhappily. If this is not possible then `std::execution::when_all` will never need to request that the other operations stop, and will therefore never need to call `std::inplace_stop_source::request_stop`, and will therefore not need access to a `std::inplace_stop_source`.

On first consideration it might seem that whether or not a `std::inplace_stop_source` is created in the above scenario is QoI, but this is not the case. Creating a `std::inplace_stop_source` and passing its stop tokens into child operations is an observable side effect (since child operations observe this through their receiver’s environment). This means that the above scenario not only leads to a `std::inplace_stop_source` being created and used for no reason, but also that it can lead to `std::execution::set_stopped_t()` completion signatures being generated where they’re never used, and where the user might expect otherwise.

### Examples

Consider an lvalue-connectable sender `s`. `s` has the following completion signatures when connected in an environment with an unstoppable token:

```cpp
std::execution::completion_signatures<
  std::execution::set_value_t()> 
```

And the following completion signatures when connected in an environment with a stoppable token:

```cpp
std::execution::completion_signatures<
  std::execution::set_value_t(),
  std::execution::set_stopped_t()> 
```

Under the status quo both `std::execution::when_all(s)` and `std::execution::when_all(s, s)` produce an lvalue-connectable sender with the following completion signatures when connected in an environment with an unstoppable token:

```cpp
std::execution::completion_signatures<
  std::execution::set_value_t(),
  std::execution::set_stopped_t()> 
```

A bizarre outcome since the second completion signature will never be used.

### Uncertainty of Sender Response

Just because a sender emits `std::execution::set_stopped_t()` when connected in an environment with a stop token does not mean that it responds to stop requests delivered through said token. For example given:

* A sender `s`, and
* A stoppable token `t`

Consider (note [3] at 26:34):

```cpp
std::execution::unstoppable(s | also_stopped_by(t)) 
```

The resulting sender advertises `std::execution::set_stopped_t()` and emits said completion in response to stop requests flowing via `t` but does not respond to stop requests from the stop token provided in the receiver’s environment.

This is not the only example of a sender which belies naïve expectations vis-à-vis stop tokens. Consider:

```cpp
s | std::execution::let_stopped([]() noexcept {
  return std::execution::just();
}) 
```

The resulting sender does not advertise `std::execution::set_stopped_t()`, but nonetheless it is responsive to stop requests.

Consider two properties of senders:

* Responds to stop requests, and
* Advertises `std::execution::set_stopped_t()`

The above examples show that the above properties are independent. That is: One cannot use the presence or absence of one of the above properties to infer the presence or absence of the other.

As pointed out to me by Lewis Baker the above is relevant vis-à-vis `std::execution::when_all`. Imagine a set of child senders provided to std::execution::when_all. Imagine that one of them is fallible. Now imagine that none of the others are responsive to stop requests. A `std::inplace_stop_source` is unnecessary since despite the fact one of the children can fail, this can’t cause the others to stop.

Unfortunately, due to the above analysis, we cannot determine whether a sender responds to stop requests, because senders do not currently publish generically-consumable information about this property.

### Synchronous Senders

It is perfectly acceptable for `std::execution`-based asynchronous operations to complete synchronously (i.e. inline with the call to `std::execution::start`). Lewis Baker points out that given a fallible sender `s``0` which completes synchronously, and any other sender `s``1`, it is not necessary for `std::execution::when_all(s``0``, s``1``)` to use a stop source. This is because `std::execution::when_all` is specified to call `std::execution::start` on operation states in the order in which the associated sender was provided. As such the asynchronous operation associated with `s``0` completes before the asynchronous operation associated with `s``1` starts. Therefore if the asynchronous operation associated with `s``0` fails the implementation of `std::execution::when_all` can simply neglect to start the asynchronous operation associated with `s``1` (note that it is perfectly allowed to create an operation state, never start it, and allow its destructor to run [4]).

Note that despite the sound logic of the above it is not (at least currently) implementable. We don’t have the ability to determine whether or not a sender’s associated asynchronous operation is synchronous [5] (beyond running the operation, by which time it’s too late to decide whether to compile the code to use a stop source).

### Proposal

#### [exec.when.all]

The following wording assumes the application of P4217R1.

[...]

The expression `when_all(sndrs...)` is expression-equivalent to:

* `make-sender``(when_all, {}, sndrs...)` if `sizeof`…`(sndrs)` is not `0`greater than `1`,
* `sndrs...[0]` if `sizeof...(sndrs)` is `1`, or
* `just()` otherwise.

[...]

The member `impls-for``<when_all_t>``::``get-env` is initialized with a callable object equivalent to the following lambda expression:

```cpp
[]<class State, class Rcvr>(auto&&, State& state, const Receiver& rcvr) 
noexcept {
  if constexpr (State::uses-stop-source) {
    return make-when-all-env(state.stop-src, get_env(rcvr));
  } else {
    return get_env(rcvr);
  }
} 
```

The member `impls-for``<when_all_t>``::``get-state` is initialized with a callable object equivalent to the following lambda expression:

```cpp
[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(noexcept(e)) ->
  decltype(e)
{
  return e;
} 
where e is the expression 
std::forward<Sndr>(sndr).apply(make-state<Rcvr>()) 
and where make-state is the following exposition-only class template: 
```

```cpp
enum class disposition { started, error, stopped };        // exposition only

template<class Rcvr>
struct make-state {
  template<class... Sndrs>
  auto operator()(auto, auto, Sndrs&&... sndrs) const {
    using values_tuple = see below;
    using errors_variant = see below;
    using stop_callback = stop_callback_for_t<
      stop_token_of_t<env_of_t<Rcvr>>, on-stop-request>;
    static constexpr bool has-stop-source = see below;     // exposition only

    struct state-type {
      void arrive(Rcvr& rcvr) noexcept {                   // exposition only
        if (0 == --count) {
        complete(rcvr);
        }
      }

      void complete(Rcvr& rcvr) noexcept;                  // exposition only

      atomic<size_t> count{sizeof...(sndrs)};              // exposition only
      inplace_stop_source stop_src{};                      // exposition only
      atomic<disposition> disp{disposition::started};      // exposition only
      errors_variant errors{};                             // exposition only
      values_tuple values{};                               // exposition only
      optional<stop_callback> on_stop{nullopt};            // exposition only
    };

    return state-type{};
  }
}; 
```

Let `copy-fail` be `exception_ptr` if decay-copying any of the child senders' result datums can potentially throw; otherwise, `none-such`, where `none-such` is an unspecified empty class type.

The alias `values_tuple` denotes the type `tuple<value_types_of_t<Sndrs,` `FWD-ENV-T``(env_of_t<Rcvr>),` `decayed-tuple``,` `optional>...>` if that type is well-formed; otherwise, `tuple<>`.

The alias `errors_variant` denotes the type `variant<``none-such``,` `copy-fail``, Es...>` with duplicate types removed, where `Es` is the pack of the decayed types of all the child senders' possible error result datums.

The static data member `uses-stop-source` is `true` if:

* `errors_variant` does not denote the type `variant<``none-such``>`, or

* There exists an element `S` of `Sndrs` such that `completion_signatures_of_t<S,`
`env_of_t<Rcvr>>` contains `set_stopped_t()`
`false` otherwise.

The member `void` `state-type``::``complete``(Rcvr& rcvr) noexcept` behaves as follows:

[...]

* Otherwise, evaluates:
`if constexpr (``uses-stop-source` `&&` `sends-stopped``) {`
  `on_stop.reset();`
  `set_stopped(std::move(rcvr));`
`}`
where `sends-stopped` equals `true` if and only if there exists an element `S` of `Sndrs`
such that `completion_signatures_of_t<S,` `when-all-env``<Env>>` contains
`set_stopped_t()`.

The member `impls-for``<when_all_t>``::``start` is initialized with a callable object equivalent to the following lambda expression:

```cpp
[]<class State, class Rcvr, class... Ops>(
    State& state, Rcvr& rcvr, Ops&... ops) noexcept -> void {
  if constexpr (state.uses-stop-source) {
    state.on_stop.emplace(
      get_stop_token(get_env(rcvr)),
     on-stop-request{state.stop_src});
  }
  (start(ops), ...);
} 
```

The member `impls-for``<when_all_t>``::``complete` is initialized with a callable object equivalent to the following lambda expression:

[...]

## Implementation Experience

This paper has been implemented against nVidia’s reference implementation of `std::execution` [6].

## Acknowledgments

The author would like to thank Lewis Baker for productive discussion regarding this problem.

## References

[1] R. Leahy. when_all() is just just() P4217R1 [2] https://github.com/intel/cpp-baremetal-senders-and-receivers [3] R. Leahy. Evolving C++ Networking With Senders & Receivers (Part 2) Core C++ 2024 [4] L. Baker et al. Eliminating heap-allocations in sender/receiver with connect()/start() as basis operations P2006R1 [5] M. Nadolski. A sender query for completion behavior P3206R0 [6] https://github.com/NVIDIA/stdexec/pull/2124
