---
title: "std::execution::sequence"
document: P4320R0
date: 2026-07-15
audience: SG1, LEWG
reply-to:
  - "Robert Leahy <rleahy@rleahy.ca>"
---

Note: This is a new section.

`sequence` adapts any number of input senders into a sender which completes when all input senders have completed, with each input sender’s asynchronous operation being started only after the completion of the preceding input sender’s asynchronous operation.

The name `sequence` denotes a customization point object. Let `sndrs` be a pack of subexpressions and let `Sndrs` be a pack of the types `decltype((sndrs))...`. The expression `sequence(sndrs...)` is ill-formed if `(sender<Sndrs> && ...)` is `false`.

Otherwise, the expression `sequence(sndrs...)` is expression-equivalent to:

* `just()` if `sizeof...(sndrs)` is `0`,
* `sndrs...` if `sizeof...(sndrs)` is `1`, otherwise
* `make-sender``(sequence, {}, sndrs...)`

Let `sequence-tag` denote a unique, empty class type.

Let the expression `sequence.transform_sender(s, es...)` be expression-equivalent to:

```cpp
make-sender(
  sequence-tag{}, tuple(std::forward_like<decltype((s))>(sndrs)...)) 
```

Where `sndrs` denotes the pack which would be declared by:

```cpp
auto&& [_, _, ...sndrs] = s; 
```

The exposition-only class template `impls-for` ([exec.snd.expos]) is specialized for `sequence-tag` as follows:

```cpp
namespace std::execution { 
  template<> 
    struct impls-for<sequence-tag> : default-impls {
      static constexpr auto get-state = see below; 
      static constexpr auto start = see below;

      template<class Sndr, class... Env>
        static consteval void check-types(); 
    }; 
} 
template<class Sndr, class... Env>
  static consteval void check-types(); 
```

Let `Is` be the pack of integral template arguments of the `integer_sequence` specialization denoted by `indices-for``<Sndr>`.

*Effects:* Equivalent to:

```cpp
auto fn = []<class Child, size_t I>() {
  auto fn = []<class QualifiedChild>() {
    auto cs = get_completion_signatures<QualifiedChild, Env...>();
    constexpr bool last = I != sizeof...(Is) - 1;
    using value_completions = gather-signatures<
      set_value_t, decltype(cs), tuple, variant-or-empty>>;
    if constexpr (!(last || is_same_v<value_completions, variant<tuple<>>>))
    {
      throw unspecified-exception{};
    }
  };
  if constexpr (I) {
    fn.template operator()<Child>();
  } else {
    using type = remove_cvref_t<Child>;
    if constexpr (!is_constructible_v<Child, type>) {
      throw unspecified-exception{};
    }
    fn.template operator()<type>();
  }
};
(fn.template operator()<child-type<Sndr, Is>, Is>(), ...); 
```

Let `sequence-state` denote the following exposition-only class template:

```cpp
template<class Rcvr, class Sndr, class... Sndrs>
struct sequence-state {
  template<size_t I>
  struct receiver {             // exposition only
    using receiver_concept = receiver_tag;

    sequence-state& state;      // exposition only
    Rcvr& rcvr;                 // exposition only

    template<class... Args>
    constexpr void set_value(Args&&... args) noexcept {
     state.impl<I>(rcvr, execution::set_value, std::forward<Args>(args)...);
    
    template<class... Args>
    constexpr void set_error(Args&&... args) noexcept {
     state.impl<I>(rcvr, execution::set_error, std::forward<Args>(args)...);
    }
    template<class... Args>
    constexpr void set_stopped(Args&&... args) noexcept {
     state.impl<I>(
        rcvr, execution::set_stopped, std::forward<Args>(args)...);
    }

    constexpr env_of_t<const Rcvr&> get_env() const noexcept {
      return execution::get_env(rcvr);
    }
  };

  variant<
    connect_result_t<Sndr, receiver<0>>,
    see below> ops;             // exposition only
  tuple<Sndrs...> sndrs;        // exposition only

  template<size_t I, class Tag, class... Args>
  constexpr void impl(Rcvr& rcvr, Tag tag, Args&&... args) noexcept {
    constexpr bool last = I == sizeof...(Sndrs);
    constexpr bool success = is_same_v<Tag, set_value_t>;
    if constexpr (last || !success) {
      tag(std::move(rcvr), std::forward<Args>(args)...);
    } else {
      auto&& next = std::get<I>(sndrs);
     receiver<I + 1> r{rcvr, *this};
      constexpr bool nothrow = noexcept(
        connect(std::move(next), std::move(r)));
      auto mkop = [&] {
        return connect(std::move(next), std::move(r));
      };
      try {
        auto& op = ops.template emplace<I + 1>(emplace-from{mkop});
        start(op);
      } catch (...) {
        if constexpr (nothrow) {
          set_error(std::move(rcvr), current_exception());
```

```cpp
        }
      }
    
  }

  template<class... Ts>
  constexpr sequence-state(Rcvr& rcvr, Sndr&& sndr, Ts&&... ts)
    noexcept(
      (is_nothrow_constructible_v<Sndrs, Ts> && ...) &&
      noexcept(connect(declval<Sndr>(), declval<receiver<0>>()))
    : ops(in_place_index<0>,
         emplace-from{[&] {
            return connect(
              std::forward<Sndr>(sndr), receiver<0>{rcvr, *this});
          }),
     sndrs(std::forward<Ts>(ts)...) {}
}; 
```

Let `Is` be a pack of the template arguments of the type denoted by `index_sequence_for<Sndrs...>`. The unspecified template arguments of the type of the `ops` exposition-only member of template specializations of `sequence-state` are the types contained by the pack `connect_result_t<Sndrs, receiver<Is + 1>>...`.

The member `impls-for``<sequence_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(see below) {
  auto&& [_, tuple] = std::forward<Sndr>(sndr);
  auto&& [sndr, ...sndrs] = std::forward<decltype(tuple)>(tuple);
  return sequence-state<
    Rcvr,
    decltype(sndr),
    remove_reference_t<decltype(sndrs)>>(
      rcvr,
      std::forward<decltype(sndr)>(sndr),
      std::forward<decltype(sndrs)>(sndrs)...);
} 
```

The expression in the `noexcept` clause is equivalent to `noexcept(`*e*`)` where *e* is the expression evaluated by the `return` statement.

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

```cpp
[](auto& state, auto&) noexcept -> void {
  execution::start(get<0>(state.ops));
```

### Open Design Questions

* Should `std::execution::sequence` lazily connect successive operations (status quo
of this proposal) or eagerly connect operations when it is connected?

* Should `std::execution::sequence` be specified through recursive composition of a
binary version thereof?

## Implementation Experience

This algorithm is provided by Nvidia’s stdexec [3].

## References

[1] M. Dominiak et al. std::execution P2300R10 [2] https://github.com/facebookexperimental/libunifex/blob/effb7527401b32b5a2d82fdf6d1a8e8810 cbdb07/doc/api_reference.md#sequencesender-predecessors-sender-last---sender [3] https://github.com/NVIDIA/stdexec/blob/711da5971a8e8e940763c11bf6bbeb1c1bb22c3a/includ e/stdexec/__detail/__sequence.hpp [4] B. Lelbach. C++ Asynchronous Parallel Algorithms P3300R0
