diff --git a/docs/CONTRIBUTING-docs.md b/docs/CONTRIBUTING-docs.md new file mode 100644 index 000000000..d399e49de --- /dev/null +++ b/docs/CONTRIBUTING-docs.md @@ -0,0 +1,537 @@ +# Contributing to stdexec's documentation + +This guide is for anyone adding or editing API reference documentation, +user-guide content, or developer-guide content in stdexec. The goal is +that two contributors documenting different CPOs in parallel produce +documentation that looks like it was written by one person. + +If you're documenting **one new CPO** end-to-end and need the short +version, jump straight to [the checklist](#checklist-adding-a-new-cpo). + +## Where the docs live + +Documenting a CPO touches up to three files: + +| File | What lives here | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `include/stdexec/__detail/.hpp` | Doxygen comments on the CPO type (`foo_t`) and its `inline constexpr foo_t foo{}` variable. | +| `docs/source/reference/index.rst` | A short orienting paragraph, a `_ref-` label, and `breathe` directives to pull the Doxygen XML in. | +| `docs/source/user/index.rst` *(consumer-facing)* | An approachable, example-driven section keyed by a `_UserGuide_` label. Skip if the CPO is internal. | +| `docs/source/developer/index.rst` *(extender-facing)* | Only when the CPO is relevant to extending stdexec (custom algorithms, domains, schedulers, etc.). | + +The build pipeline is `CMake → Doxygen → Sphinx (with Breathe)`. Doxygen +parses C++ headers into XML; Breathe pulls those XML entries into +Sphinx via directives in the `.rst` files. To rebuild locally: + +```sh +cmake -B build/docs -S . -DSTDEXEC_BUILD_DOCS=ON \ + -DSTDEXEC_BUILD_EXAMPLES=OFF -DSTDEXEC_BUILD_TESTS=OFF +cmake --build build/docs --target docs +# Output: build/docs/docs/index.html +``` + +## The CPO doc anatomy + +Every public CPO has the same anatomy. Use [`__then.hpp`](../include/stdexec/__detail/__then.hpp) +and the rendered [`then` reference entry](./source/reference/index.rst) as +the canonical template. Each section below is a heading you should +include — in this order — unless the section truly does not apply. + +### 1. `@brief` on the type + +One sentence. Describes what the adaptor *does* in the abstract, not +*how*. Lead with "A pipeable sender adaptor that …" for adaptors, +"A sender factory that …" for factories, "A sender consumer that …" for +consumers. + +```cpp +//! @brief A pipeable sender adaptor that transforms a predecessor sender's +//! value completion by invoking a callable on the values it produces. +struct foo_t { /* … */ }; +``` + +### 2. Detailed description (prose) + +Two to four paragraphs of approachable prose immediately after `@brief`. +Cover *what it does* and *why you'd reach for it*, not internals. Include +both call syntaxes (direct and pipe) in a small `@code{.cpp}` block. End +with a pointer to the normative spec: `See [exec.foo] in the C++26 +working draft for the normative specification.` + +### 3. Inline section headings + +Use **bold inline headings** for sub-sections — not `@par`, not `###`. + +```cpp +//! **Completion signatures.** +//! +//! Prose here, including code blocks, can span multiple paragraphs… +``` + +> ⚠️ **Why not `@par`?** Doxygen's `@par Title` only spans *one paragraph*, +> so any multi-paragraph section (which most are, because they include +> code blocks) leaks past the heading. Worse, the prose ends up +> rendered *before* the heading. +> +> ⚠️ **Why not markdown `###`?** Breathe 4.35 crashes on `docSect2TypeSub` +> nodes inside member docs. + +Standard sub-section headings, in order: + +1. `**Completion signatures.**` — Two code blocks (input sender → output + sender), then any text explaining multiple-value-completion cases, + `void`-returning callables, etc. +2. `**Exception behavior.**` — What happens if the callable throws or + the predecessor errors out. +3. `**Cancellation.**` — Stop-token interaction and stopped-completion + forwarding. +4. `**Example.**` — One complete, compilable example with `#include + `, a `main()`, and an `assert(…)` showing the + expected result. + +Omit a section only when there is genuinely nothing meaningful to say +(rare). Don't include a section as a stub. + +### 4. `@see` cross-refs to related CPOs + +End the type-level comment with `@see` lines pointing to closely related +CPOs. Use a single em-dash to introduce each one-line description: + +```cpp +//! @see stdexec::upon_error — adapt the error channel +//! @see stdexec::upon_stopped — adapt the stopped channel +//! @see stdexec::let_value — adapt the value channel with a sender-returning function +``` + +Doxygen autolinks fully-qualified names that are also documented. +If the cross-referenced CPO isn't documented yet, leave the `@see` +in — it'll start resolving as soon as that CPO is documented. + +### 5. Per-overload comments + +Each `operator()` overload gets its own short comment block with +`@brief`, `@tparam`, `@param`, `@returns`, and any `@pre`: + +```cpp +//! @brief Construct a sender that adapts @c __sndr by invoking @c __fun +//! with each value-completion argument pack it produces. +//! +//! @tparam _Sender A type satisfying the @c stdexec::sender concept. +//! @tparam _Fun A decayed, move-constructible callable type +//! (satisfying the internal __movable_value concept). +//! +//! @param __sndr The predecessor sender ... +//! @param __fun The function (or callable) to invoke ... +//! +//! @returns A sender that, when connected to a receiver and started, ... +//! +//! @pre @c __fun must be invocable with every value-completion argument +//! pack of @c __sndr ... +template +constexpr auto operator()(_Sender&& __sndr, _Fun __fun) const + -> __well_formed_sender auto; +``` + +Document **every overload**, including the pipeable (unary) form. + +### 6. `@brief` on the inline variable + +Keep this short — the substantive documentation lives on the type. + +```cpp +//! @brief The customization point object for the @c foo sender adaptor. +//! +//! @c foo is an instance of @ref foo_t. See @ref foo_t for the full +//! description, the completion-signature transformation rules, exception +//! and cancellation behavior, and a usage example. +//! +//! @hideinitializer +inline constexpr foo_t foo{}; +``` + +`@hideinitializer` suppresses Doxygen from dumping `{}` into the rendered +docs. + +## Tag conventions + +| Tag | Use it for | Notes | +| ------------------ | ------------------------------------------------------- | ------------------------------------------------------ | +| `@brief` | One-sentence summary on every documented entity | Required. | +| `@tparam` | Each template parameter | Use the underscore-prefixed name (`_Sender`, `_Fun`). | +| `@param` | Each function parameter | Use the underscore-prefixed name (`__sndr`, `__fun`). | +| `@returns` | The return value | Prefer `@returns` over `@return` for symmetry. | +| `@pre` | Preconditions the caller must satisfy | Use for ill-formed-program conditions, e.g. invocability. | +| `@see` | Cross-references to related CPOs | Fully qualify (`stdexec::let_value`). | +| `@code{.cpp}` | C++ example blocks | Always include the language tag for syntax highlighting. | +| `@c` | Inline ``-styled identifiers in prose | `@c f`, `@c sndr`. Prefer this to backticks. | +| `` | Inline code containing characters `@c` chokes on | E.g. `then(sndr, f)` (has space and parens). | +| `@hideinitializer` | Suppress `{}` from the rendered `inline constexpr ...` | Use on every CPO inline-variable doc. | + +Tags **not to use**: `@par` (multi-paragraph bug), `### markdown headings` +(breathe crash), `@param[in]` / `@param[out]` (we're const-correct via the +type, not via parameter direction tags). + +## Documenting a function template (non-CPO) + +A few public symbols are *function templates*, not customization-point +objects (e.g. `stdexec::get_completion_signatures()`). They have +no underlying struct, no `operator()`, and may be overloaded. + +For these, neither `doxygenstruct` nor `doxygenvariable` applies, and +`doxygenfunction:: stdexec::name(args)` is finicky: + +- For overloaded templates, breathe must disambiguate by signature. + Specifying `doxygenfunction:: stdexec::name()` is ambiguous when + multiple overloads have the same `argsstring`, and breathe fails with + *"Unable to resolve function ... with arguments ()"*. +- For consteval templates whose entire signature is in template + parameters, there's no string-level disambiguator. + +The simplest workaround: render the *file's* documentation block with +`doxygenfile`: + +```rst +.. doxygenfile:: __get_completion_signatures.hpp + :sections: briefdescription detaileddescription +``` + +This pulls in the comments from the file and renders them inline; you +lose the per-overload separation but get the prose you wrote. Pair it +with explicit prose in the surrounding RST that names the two flavors +(`()` and `()`) so readers know what they're looking +at. + +## Documenting a C++20 concept + +Concepts get the same anatomy as CPOs (brief, prose, requirements, +`@see`), with two differences: + +- The concept itself is the target of the doxygen comment — there's no + associated "type" page like there is for a CPO. Put all the + documentation directly on the concept template. +- In the `.rst`, use `.. doxygenconcept::` (not `doxygenstruct`): + + ```rst + .. _ref-concept-foo: + + ``foo`` + ^^^^^^^ + + .. doxygenconcept:: stdexec::foo + ``` + +When a concept depends on a tag type (e.g. `stdexec::sender_tag` for +`stdexec::sender`), document the tag type with `doxygenstruct` right +after the concept so both appear together. + +## Cross-referencing + +Between the three documentation files, links flow in both directions: + +- **`.rst` → Doxygen entity**: use Sphinx C++ domain roles. + `:cpp:member:`stdexec::foo`` for the inline variable, + `:cpp:struct:`stdexec::foo_t`` for the type, + `:cpp:func:`stdexec::foo_t::operator()`` for a specific overload. +- **Reference section ↔ User Guide**: define labels and reference them. + In reference: `.. _ref-foo:` immediately before the section heading. + In user guide: `.. _UserGuide_foo:` immediately before the section + heading. Cross-reference with the **explicit-text form** of `:ref:`: + + ```rst + see :ref:`sync_wait ` for blah ... + ``` + + Avoid the bare form ``:ref:`ref-sync_wait` `` — even when the label + is followed by a section heading, some such references fail with + "A title or caption not found", apparently when other Sphinx-domain + entities (e.g. `doxygenstruct`-rendered C++ symbols) get indexed + against the same label. The explicit-text form bypasses the title + lookup entirely and always works. +- **Doxygen → Doxygen**: `@see stdexec::other_cpo` autolinks when both + are documented. Use `@ref foo_t` for an explicit ref to a struct/class. + +## Reference section pattern + +For each CPO, add a sub-heading and these directives to +`docs/source/reference/index.rst`: + +```rst +.. _ref-foo: + +``foo`` — one-line summary of what foo does +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +One short paragraph orienting the reader, with a forward-link to the +User's Guide: see :ref:`UserGuide_foo` for an approachable introduction +with worked examples; the full reference follows. + +.. doxygenstruct:: stdexec::foo_t + :members: + +.. doxygenvariable:: stdexec::foo +``` + +The `:members:` option on `doxygenstruct` is essential — without it, +Breathe renders only the brief and omits the `operator()` overloads. + +Group adaptors by which completion channel they primarily affect (value +/ error / stopped), and within each group, list factory → adaptor → +consumer roles. + +## User Guide section pattern + +```rst +.. _UserGuide_foo: + +``foo`` — one-line user-facing tagline +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::foo` is the asynchronous counterpart to "…". You +give it … and you get back …. + +The simplest possible example: + +.. code-block:: cpp + + auto sndr = stdexec::just(21) + | stdexec::foo([](int x) { return x * 2; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 +``` + +The structure to follow: + +1. One paragraph: what it is and what you'd reach for it for. +2. Simplest possible example. +3. The two call syntaxes (direct vs. pipe). +4. A more realistic example: chained transformations, mixed types, etc. +5. **What happens on error?** (one paragraph) +6. **What happens on cancellation?** (one paragraph) +7. **When *not* to use it** — explicitly point readers to the alternative + they probably want (e.g. `let_value` instead of `then` for + sender-returning functions). + +Bold-headed mini-sections are fine in RST; use the **`**Bold.**`** +convention so the visual style matches the Doxygen-rendered sections. + +## Checklist: adding a new CPO + +For each new CPO `foo`, in order: + +1. ☐ Add Doxygen on the `foo_t` type per [the anatomy](#the-cpo-doc-anatomy). +2. ☐ Add Doxygen on each `operator()` overload. +3. ☐ Add a short `@brief` + `@ref foo_t` + `@hideinitializer` on the + `inline constexpr foo_t foo{}` variable. +4. ☐ Add the `.. _ref-foo:` block to `docs/source/reference/index.rst`. +5. ☐ Add the `.. _UserGuide_foo:` block to `docs/source/user/index.rst` + (if the CPO is consumer-facing). +6. ☐ Add a developer-guide entry to `docs/source/developer/index.rst` + only if the CPO is relevant to extending the framework. +7. ☐ Run `cmake --build build/docs --target docs` and confirm: + - Build succeeds with no *new* warnings (3 pre-existing warnings are + OK: 2 intersphinx-network, 1 `abi-breaks.md` toctree). + - The new section renders with no empty `

` blocks (sign + of misused `@par`). + - Cross-refs in both directions resolve to working links (no + `??` or unresolved `:cpp:member:` warnings). + +## Special case: CPOs whose underlying type is `__`-prefixed + +A few CPOs are instances of types that themselves start with a double +underscore — e.g. `read_env` is `inline constexpr __read_env_t read_env{}`. +Because `EXCLUDE_SYMBOLS = *__*` hides the type, there is *no* `_t` page +for Breathe to surface; the only public symbol Sphinx can render is the +`inline constexpr` variable. + +In this situation: + +- **Omit the `doxygenstruct` directive** from the reference `.rst`. The + underlying type isn't documented and shouldn't appear in the index. +- **Put the substantive prose on the `inline constexpr` variable** + directly — `@brief`, the long description, *Completion signatures*, + *Exception behavior*, *Cancellation*, *Example*, `@see`. Add + `@hideinitializer` to suppress the `{}` from the rendered signature. +- **Skip per-overload `@tparam`/`@param`/`@returns` blocks** — there's no + type-level page to attach overload documentation to. Describe the call + form inline in the type-level prose (a `@code{.cpp}` block showing + `read_env(q)` is usually enough). +- The rendered signature will leak the internal type name (e.g. + `__read_env_t const stdexec::read_env`). This is a known cosmetic + wart — accept it, or rename the type publicly if the CPO is important + enough. + +`read_env` is the canonical example of this pattern. + +## Special case: CPOs that inherit `operator()` from a private base + +Some CPO types share their `operator()` overloads by inheriting from a +common implementation base — e.g. `let_value_t` / `let_error_t` / +`let_stopped_t` all inherit `operator()` from `__let::__let_t<…>`. Because +`EXCLUDE_SYMBOLS = *__*` hides the base, Breathe's `:members:` option will +only surface the (boring) defaulted default-constructor on the derived +type — not the overloads that are the actual API. + +In this situation: + +- **Drop `:members:`** from the `doxygenstruct` directive in the reference + `.rst`. Listing the defaulted constructor is just noise. +- **Show the call signatures inline in the type-level prose** using a + `@code{.cpp}` block before the *Completion signatures* section, e.g.: + + ```cpp + //! The signature of the operator overloads (inherited from a detail base) is: + //! + //! @code{.cpp} + //! template + //! auto operator()(Sender&& sndr, Fun fun) const -> sender auto; // direct + //! + //! template + //! auto operator()(Fun fun) const; // closure + //! @endcode + ``` +- **Skip the per-overload `@tparam` / `@param` / `@returns` blocks** — + there's nowhere for them to attach. Put any precondition info in the + type-level prose instead. + +You can usually spot this case at a glance: the derived type's struct body +contains only a `using` and a defaulted constructor. + +## Sphinx anchor normalization + +Sphinx slugifies labels for HTML IDs: `_ref-let_value:` in the RST becomes +`id="ref-let-value"` in the rendered HTML. Two practical consequences: + +- When grepping the rendered HTML, search for the dashed form + (`ref-let-value`, `userguide-then`), not the underscored form from the + `.rst` label. +- Cross-references in RST (`:ref:`UserGuide_let_value``) still use the + original underscored label — Sphinx does the slugification. + +## Toolchain wrinkles you might trip over + +### Pragma macros confuse the doxygen parser + +`STDEXEC_PRAGMA_IGNORE_GNU(...)`, `STDEXEC_PRAGMA_PUSH()`, +`STDEXEC_PRAGMA_POP()`, etc. expand (via `_Pragma`) to GCC/Clang +pragma operators. When one of these macros sits at file scope ahead of +a heavily-commented type definition, doxygen's preprocessor can fail to +recognize the subsequent struct as a class, leaving you with a hpp-level +XML file that has no `innerclass` entries — and breathe will then warn +that it cannot find `stdexec::your_t`. + +The fix is in the Doxyfile (`docs/Doxyfile.in`): predefine each pragma +macro to an empty token sequence so doxygen's preprocessor strips it +out entirely. The current set is: + +``` +"STDEXEC_PRAGMA_PUSH()= " +"STDEXEC_PRAGMA_POP()= " +"STDEXEC_PRAGMA_IGNORE_GNU(X)= " +"STDEXEC_PRAGMA_IGNORE_EDG(X)= " +"STDEXEC_PRAGMA_IGNORE_MSVC(X)= " +``` + +If you add a new pragma macro elsewhere in the codebase, add it to +`PREDEFINED` in the Doxyfile too — otherwise the first contributor who +documents a type below it will see breathe warnings and wonder why. + +### Variadic macros need `(...)` in PREDEFINED, not `(X)` + +When predefining a function-like macro, the parameter list must *match the +arity of the actual macro*. `STDEXEC_ATTRIBUTE` is defined in +`__config.hpp` as `STDEXEC_ATTRIBUTE(...)` (variadic) and is called both +as `STDEXEC_ATTRIBUTE(always_inline)` and as +`STDEXEC_ATTRIBUTE(host, device)`. If the Doxyfile predefines it as +`STDEXEC_ATTRIBUTE(X)=` (single-arg), the two-arg call mis-substitutes +and leaks the second argument into the surrounding signature — you end +up with rendered prototypes like + +``` +device constexpr auto stdexec::just_t::operator()(_Ts &&... __ts) const +``` + +which Sphinx's C++ domain then refuses to parse, surfacing as + +``` +reference/index.rst:NN: WARNING: Error when parsing function declaration. +``` + +— annoyingly, the source location Sphinx reports is the `.rst` line of +the `doxygenstruct` directive, *not* the offending macro. So if you see +that warning, suspect a PREDEFINED macro-arity mismatch, not the `.rst`. + +The fix is in the Doxyfile: predefine the macro with `(...)` so it +absorbs any number of arguments: + +``` +"STDEXEC_ATTRIBUTE(...)= " +``` + +If you introduce a new variadic macro in the codebase, mirror it in +`PREDEFINED` the same way. + +## Pitfalls (the short list) + +- **Don't use `@par`** — use `**Bold inline headings.**` instead. +- **Don't use markdown `###` inside member docs** — breathe crashes. +- **Don't forget `:members:` on `doxygenstruct`** — overloads vanish. + *Exception:* when the operator overloads come from an excluded base, + drop `:members:` and document the signatures in prose (see above). +- **Don't put the substantive doc on the `inline constexpr` variable** — + `doxygenvariable` doesn't render the type's `operator()` overloads. Put + the long description on the type and a brief pointer on the variable. +- **Don't document internals** — symbols matching `*__*` are already + excluded by the Doxyfile (`EXCLUDE_SYMBOLS = *__*`). Don't fight this + by giving an `_Impl`-suffixed type its own brief. +- **Beware `@ref` followed by punctuation.** Doxygen consumes the next + token wholesale, so `@ref starts_on_t:` is parsed as the symbol name + `starts_on_t:` (with the colon) and fails to resolve. Prefer an em-dash + or comma with a leading space (`@ref starts_on_t — note: …`), or use + the explicit-text form `@ref starts_on_t "starts_on_t"`. +- **No Unicode in `@code{.cpp}` blocks.** Pygments' C++ lexer can't + tokenize `…`, `→`, em-dashes inside code, etc., and Sphinx warns + about "Lexing literal_block ... resulted in an error at token …". + Use ASCII (`...`, `->`, `--`) inside code blocks. Em-dashes etc. are + fine in prose. +- **No `X` for templated names.** Doxygen's HTML parser + treats the inner `` as an HTML tag (and a single-capital `` + becomes a strikethrough), corrupting the rest of the comment block. + Symptoms include "found `` tag while expecting ``" warnings, + doxygen losing track of subsequent class definitions in the file + (resulting in `Cannot find class "stdexec::foo_t"` warnings from + breathe), and concept/struct symbols losing their `stdexec::` + namespace in the XML. Use **markdown backticks** for any inline code + containing angle brackets: write `` `enable_sender` `` instead of + `enable_sender`. (Backticks are opaque to doxygen's HTML + parser.) +- **No `/* ... */` inside `@code{.cpp}` blocks.** Doxygen's + comment scanner gets confused by `*/` even inside what it should + treat as opaque code, and reports "reached end of file while inside + a 'code' block". Use `// ...` line comments inside example code. + +## Marking a CPO as deprecated + +When a CPO is deprecated (e.g. `transfer_when_all`), document it both +in the doxygen comment and in the `.rst`: + +- **In the doxygen comment**, use `@deprecated` with a one-line + recommendation pointing at the replacement. Note that Doxygen + captures `@deprecated` as an `xrefsect` and renders it on a *global* + "Deprecated List" page — **it does not appear inline on the struct's + reference page**. So `@deprecated` alone is not enough for visibility. +- **In the `.rst`**, add a `.. admonition::` block right above the + `doxygenstruct` directive so the warning renders inline: + + ```rst + .. admonition:: Deprecated + :class: warning + + This adaptor is not part of the C++26 working draft and is retained + only for backwards compatibility. Write + ``when_all(sndrs...) | continues_on(sch)`` instead. + ``` + + Don't use `.. deprecated::` — that directive treats the *first word* + of its argument as the "since version" string, so + `.. deprecated:: This adaptor is …` renders as "Deprecated since + version This: adaptor is …", which is gibberish. diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index 1000b4ed1..0eff39f36 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -2370,7 +2370,7 @@ PREDEFINED = "__cplusplus=202302L" \ "STDEXEC=stdexec" \ "STDEXEC_PARALLEL_SCHEDULER_HEADER_ONLY=1" \ "STDEXEC_PARALLEL_SCHEDULER_INLINE=inline" \ - "STDEXEC_ATTRIBUTE(X)= " \ + "STDEXEC_ATTRIBUTE(...)= " \ "STDEXEC_IMMOVABLE_NO_UNIQUE_ADDRESS= " \ "STDEXEC_AUTO_RETURN(...)=->decltype(auto){ return __VA_ARGS__; }" \ "STDEXEC_EXPLICIT_THIS_BEGIN(...)=__VA_ARGS__" \ @@ -2379,7 +2379,12 @@ PREDEFINED = "__cplusplus=202302L" \ "STDEXEC_MSVC()=0" \ "STDEXEC_GCC()=0" \ "STDEXEC_NVHPC()=0" \ - "STDEXEC_EDG()=0" + "STDEXEC_EDG()=0" \ + "STDEXEC_PRAGMA_PUSH()= " \ + "STDEXEC_PRAGMA_POP()= " \ + "STDEXEC_PRAGMA_IGNORE_GNU(X)= " \ + "STDEXEC_PRAGMA_IGNORE_EDG(X)= " \ + "STDEXEC_PRAGMA_IGNORE_MSVC(X)= " # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/docs/source/developer/index.rst b/docs/source/developer/index.rst index ecaa7ff43..86e18b7e3 100644 --- a/docs/source/developer/index.rst +++ b/docs/source/developer/index.rst @@ -130,33 +130,1158 @@ customize its behavior. Core Customization Points ------------------------- -Sender algorithms are defined in terms of **core customization points**. Below are the -core customization points that define how senders and receivers interact: +Sender algorithms are defined in terms of a small set of **core +customization points** (CPOs) — the operations that every sender, +receiver, and operation state type must support in some form. Most user +code never touches these directly; sender adaptors and consumers do. +Anyone *writing* a new sender, receiver, or scheduler will implement one +or more of these. + +The picker table below gives a one-liner per CPO; the sections that +follow expand each one with the customization patterns sender / receiver +/ operation-state authors actually use. Each CPO has a full-detail +reference entry under :ref:`Core Customization Points +` in the Reference section. .. list-table:: Core customization points + :header-rows: 1 + :widths: 32 28 40 + + * - CPO + - Lives on + - Purpose + * - :ref:`connect ` + - sender + - Connect a sender to a receiver, producing an operation state. + * - :ref:`get_completion_signatures ` + - sender (compile-time) + - Compute what signals a sender can deliver. + * - :ref:`start ` + - operation state + - Begin execution of a connected operation. + * - :ref:`set_value ` + - receiver + - Deliver a successful value completion. + * - :ref:`set_error ` + - receiver + - Deliver a typed error completion. + * - :ref:`set_stopped ` + - receiver + - Deliver a stopped (cancellation) completion. + * - :ref:`get_env ` + - sender *and* receiver + - Obtain the environment (queries: stop token, allocator, …). + +``connect`` — connect a sender to a receiver +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A sender author exposes :cpp:member:`stdexec::connect` via a +`.connect()` member that returns an +:ref:`operation_state `: + +.. code-block:: cpp + + struct my_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t(int)>; + + int value_; + + template R> + auto connect(R rcvr) && -> my_opstate { + return my_opstate{std::move(rcvr), value_}; + } + }; + +Notes: + +- The returned operation state should be returned by value (it is + immovable *after* construction, but stdexec relies on prvalue + copy-elision to actually place it). +- The member is non-`const` and accepts the sender by value or + rvalue reference — sender adaptors typically *move* their inputs + into the operation state. +- Before dispatch, the framework runs ``transform_sender`` on the + sender (passing in the receiver's environment), so domain-based + customization happens *between* the user's call and the + ``.connect()`` member. See :ref:`Customizing stdexec's algorithms + ` below. + +``get_completion_signatures`` — declare what a sender produces +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Either expose a non-template type alias ``completion_signatures`` +(for environment-independent senders — the common case): + +.. code-block:: cpp + + struct my_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t(int), + stdexec::set_error_t(std::exception_ptr)>; + // ... connect() + }; + +…or, for senders whose signatures depend on the receiver's +environment, provide a static ``consteval`` member template: + +.. code-block:: cpp + + struct env_dependent_sender { + using sender_concept = stdexec::sender_tag; + + template + static consteval auto get_completion_signatures() noexcept { + // ... compute signatures from Env... + } + // ... connect() + }; + +``start`` — begin execution +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An operation-state author provides a ``noexcept``, ``void``-returning +``start()`` member: + +.. code-block:: cpp + + template + struct my_opstate { + using operation_state_concept = stdexec::operation_state_tag; + + // Immovable after construction: + my_opstate(my_opstate&&) = delete; + + R rcvr_; + int value_; + + void start() noexcept { + stdexec::set_value(std::move(rcvr_), value_); + } + }; + +The ``noexcept`` and ``void`` return are enforced by the dispatch site +with static asserts — the operation state must commit to never +throwing out of ``start``, and there is nothing to return. + +``set_value`` / ``set_error`` / ``set_stopped`` — completion signals +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A receiver author opts into the three completion channels by exposing +matching ``noexcept`` members. Receivers typically implement *one* of +:cpp:member:`stdexec::set_value` and :cpp:member:`stdexec::set_error`, +and almost always :cpp:member:`stdexec::set_stopped`: + +.. code-block:: cpp + + struct my_receiver { + using receiver_concept = stdexec::receiver_tag; + + void set_value(int v) noexcept { /* success */ } + void set_error(std::exception_ptr e) noexcept { /* error */ } + void set_stopped() noexcept { /* cancellation */ } + }; + +Receivers may have multiple ``set_error`` overloads (one per error +type they understand), and the ``set_value`` arity must match the +sender's value-completion signatures. + +The receiver promises that **exactly one** of the three completion +signals will be called on it, exactly once, after the operation has +been started. The receiver may not be called after destruction; the +operation state is responsible for ensuring this. + +``get_env`` — expose the receiver's environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Most receivers expose an environment so child operations can query +the stop token, allocator, preferred scheduler, etc. The +:cpp:member:`stdexec::get_env` CPO retrieves it: + +.. code-block:: cpp + + struct my_receiver { + using receiver_concept = stdexec::receiver_tag; + + stop_token_t stop_token_; + + auto get_env() const noexcept { + return stdexec::env{ + stdexec::prop{stdexec::get_stop_token, stop_token_}}; + } + // ... set_value, set_error, set_stopped + }; + +The same CPO is used to query a *sender*'s attributes (e.g. its +completion scheduler) — the only difference is which member the +sender or receiver implements. A sender that has no attributes to +expose may simply omit ``get_env``; the CPO will default to an empty +environment via its ``__ignore`` overload. + +.. _building-a-custom-algorithm-simple-then: + +Building a Custom Algorithm: ``simple_then`` +-------------------------------------------- + +This section is a worked example. We'll build a hand-rolled version of +:cpp:member:`stdexec::then` from scratch, using only the +:ref:`concepts ` and :ref:`core customization +points ` documented above. By the end you'll have a +complete, compilable, ~70-line sender adaptor that the rest of stdexec +treats as a first-class citizen. + +We'll call it ``simple_then`` so it doesn't collide with the real one. +The semantics: + +.. code-block:: cpp + + auto pipeline = simple_then( + stdexec::just(21), + [](int x) { return x * 2; }); + auto [v] = stdexec::sync_wait(std::move(pipeline)).value(); + // v == 42 + +To keep the focus on the *structure* of an adaptor rather than the +type-system gymnastics, we will hardcode the completion signatures. +That's a real limitation; we'll discuss how to lift it at the end. + +The shape of an adaptor +^^^^^^^^^^^^^^^^^^^^^^^ + +Every sender adaptor is structurally three pieces, even though we'll +only have to write *two* of them. Recall the protocol: + +.. code-block:: text + + +-------- predecessor -------- our adaptor ----+ + | | + 1. caller -----> | sender ---connect---> opstate ---start---+ | + | | | + 2. | v | + | set_value/error/stopped + | | | + +------------------------------------------+---+ + | + v + receiver + +A predecessor sender is the input. When connected to a receiver and +started, it eventually completes by calling +:cpp:member:`stdexec::set_value` (or ``set_error``, or ``set_stopped``) +on its receiver. Our adaptor's job is to *intercept* the value +completion and apply our callable to it, forwarding the result. + +The trick is to *wrap the receiver*, not the sender. Our adaptor needs +three components: + +1. A **sender type** holding the predecessor and the callable. +2. A **wrapping receiver** that intercepts ``set_value`` (transforming + it) and forwards everything else verbatim to the real receiver. +3. An **operation state**. We'll get this for free: when we ``connect`` + the predecessor to our wrapping receiver, the predecessor returns + its own operation state, and we can return that directly. Many + adaptors do this; the only ones that build their own operation state + are ones that need extra storage (cancellation callbacks, child + variants, etc.). + +Step 1: the wrapping receiver +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The receiver is where the actual work happens. Its +:cpp:member:`stdexec::set_value` member runs the callable and forwards +the result; its ``set_error``, ``set_stopped``, and ``get_env`` members +just pass through to the inner receiver. + +.. code-block:: cpp + + template + struct simple_then_receiver { + using receiver_concept = stdexec::receiver_tag; + + R rcvr_; + Fn fn_; + + template + void set_value(Vs&&... vs) noexcept { + try { + stdexec::set_value( + std::move(rcvr_), + std::invoke(std::move(fn_), static_cast(vs)...)); + } catch (...) { + stdexec::set_error(std::move(rcvr_), std::current_exception()); + } + } + + template + void set_error(E&& e) noexcept { + stdexec::set_error(std::move(rcvr_), static_cast(e)); + } + + void set_stopped() noexcept { + stdexec::set_stopped(std::move(rcvr_)); + } + + auto get_env() const noexcept { + return stdexec::get_env(rcvr_); + } + }; + +Things to notice: + +- ``receiver_concept = receiver_tag`` opts the type into the + :ref:`stdexec::receiver concept `. Without this + alias, our type wouldn't satisfy the concept and the framework would + refuse to connect it. +- Every completion-signal member is ``noexcept`` and returns ``void``. + The :cpp:member:`stdexec::set_value` dispatch site enforces this with + static asserts — drop the ``noexcept`` and you get a compile error. +- If ``fn_`` throws, we catch and re-deliver via + :cpp:member:`stdexec::set_error`. This is the standard convention; it + is *why* our completion signatures include + ``set_error_t(std::exception_ptr)``. +- ``get_env`` forwards the environment of the *inner* receiver. The + predecessor sender needs to see the same stop token, allocator, + scheduler etc. as the eventual consumer — our adaptor is invisible to + environment queries. (If we wanted to *modify* the environment — say, + to inject a different stop token — this is where we'd do it.) + +Step 2: the sender +^^^^^^^^^^^^^^^^^^ + +The sender is just a value type that holds the predecessor and the +callable, plus three things that wire it into the framework: + +.. code-block:: cpp + + template + struct simple_then_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t(int), + stdexec::set_error_t(std::exception_ptr), + stdexec::set_stopped_t()>; + + Sndr sndr_; + Fn fn_; + + template + auto connect(R rcvr) && { + return stdexec::connect( + std::move(sndr_), + simple_then_receiver{std::move(rcvr), std::move(fn_)}); + } + }; + +The three wiring elements: + +- ``sender_concept = sender_tag`` opts the type into + :ref:`stdexec::sender `. +- ``completion_signatures`` (a type alias to a + ``stdexec::completion_signatures`` specialization) declares what this + sender can complete with. The framework consults this to type-check + adaptors downstream. **We've hardcoded** ``set_value_t(int)`` here + for simplicity — see *Going further* below for how to compute it + properly from the predecessor's signatures. +- The ``connect`` member is what + :cpp:member:`stdexec::connect` dispatches to. Our implementation + wraps the user's receiver in a ``simple_then_receiver`` and connects + *that* to the predecessor. The predecessor's operation state is what + comes back — we don't need our own. + +Note that ``connect`` takes ``this`` as an rvalue (``&&``). Senders are +typically *moved into* the operation state, not copied, so a sender +adaptor takes its inputs as rvalues. The framework arranges for this: +:cpp:member:`stdexec::connect` perfect-forwards both the sender and the +receiver, but the conventional sender object is short-lived (a temporary +in a pipeline). + +Step 3: a helper factory +^^^^^^^^^^^^^^^^^^^^^^^^ + +Class-template type deduction would force callers to spell out the +template parameters of ``simple_then_sender``. A one-line factory +function fixes that: + +.. code-block:: cpp + + template + auto simple_then(Sndr&& sndr, Fn&& fn) { + return simple_then_sender, std::decay_t>{ + static_cast(sndr), static_cast(fn)}; + } + +Putting it all together +^^^^^^^^^^^^^^^^^^^^^^^ + +Here is the full, compilable example: + +.. code-block:: cpp + + #include + #include + #include + #include + #include + + // ---------- The wrapping receiver ----------------------------------- + + template + struct simple_then_receiver { + using receiver_concept = stdexec::receiver_tag; + + R rcvr_; + Fn fn_; + + template + void set_value(Vs&&... vs) noexcept { + try { + stdexec::set_value( + std::move(rcvr_), + std::invoke(std::move(fn_), static_cast(vs)...)); + } catch (...) { + stdexec::set_error(std::move(rcvr_), std::current_exception()); + } + } + + template + void set_error(E&& e) noexcept { + stdexec::set_error(std::move(rcvr_), static_cast(e)); + } + + void set_stopped() noexcept { + stdexec::set_stopped(std::move(rcvr_)); + } + + auto get_env() const noexcept { + return stdexec::get_env(rcvr_); + } + }; + + // ---------- The sender ---------------------------------------------- + + template + struct simple_then_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t(int), + stdexec::set_error_t(std::exception_ptr), + stdexec::set_stopped_t()>; + + Sndr sndr_; + Fn fn_; + + template + auto connect(R rcvr) && { + return stdexec::connect( + std::move(sndr_), + simple_then_receiver{std::move(rcvr), std::move(fn_)}); + } + }; + + // ---------- The factory --------------------------------------------- + + template + auto simple_then(Sndr&& sndr, Fn&& fn) { + return simple_then_sender, std::decay_t>{ + static_cast(sndr), static_cast(fn)}; + } + + // ---------- Try it out ---------------------------------------------- + + int main() { + auto pipeline = simple_then( + stdexec::just(21), + [](int x) { return x * 2; }); + + auto [v] = stdexec::sync_wait(std::move(pipeline)).value(); + assert(v == 42); + } + +That's a complete sender adaptor. The framework treats it just like a +built-in — you can chain it with other adaptors, pass it to +:cpp:member:`stdexec::sync_wait`, run it on a scheduler with +:cpp:member:`stdexec::starts_on`, ``co_await`` it from a coroutine, etc. + +Going further +^^^^^^^^^^^^^ + +The version above leaves several things on the table — each is a +realistic next step. + +**Computing the right completion signatures.** +We hardcoded ``set_value_t(int)``. For a real ``then`` we want to +compute the output's completion signatures from the input's: each +``set_value_t(Vs...)`` of the predecessor becomes a +``set_value_t(R)`` where ``R = decltype(std::invoke(fn, Vs...))``, and +the error/stopped signatures pass through unchanged. stdexec provides +``stdexec::transform_completion_signatures`` for exactly this. Replace +the hardcoded type alias with a ``static consteval`` +:cpp:member:`stdexec::get_completion_signatures` member that calls into +that utility (the implementation of the built-in +:cpp:member:`stdexec::then` does this — see +``include/stdexec/__detail/__then.hpp``). + +**Pipe syntax.** +``simple_then(sndr, fn)`` works but ``sndr | simple_then(fn)`` does +not, because ``simple_then`` with one argument doesn't return a +*sender-adaptor closure*. The fix is an overload that captures ``fn`` +into a closure object, which stdexec supplies via +``stdexec::__closure`` (currently an internal helper) and which C++26 +calls a ``pipeable_sender_adaptor_closure``. + +**A real operation state.** +Our version inherits the predecessor's operation state. Adaptors that +need their own — to allocate a child variant, hook up a stop callback, +or store value datums across a hop — write their own operation-state +type with an :ref:`operation_state_concept ` +alias and a ``noexcept`` ``start()`` member, and connect the +predecessor into a child member at construction time. The +:ref:`Customization-points walkthrough ` sketches this +pattern. + +**Forwarding the environment with modifications.** +If your adaptor wants to *change* something the predecessor sees — say, +inject a different stop token — override ``get_env`` to return a +modified environment (e.g. via ``stdexec::env`` and ``stdexec::prop``) +instead of just forwarding ``stdexec::get_env(rcvr_)``. + +Each of these is a small extension of the same protocol — the +structure (sender, wrapping receiver, completion-signal forwarding) is +unchanged. + +.. _building-a-custom-scheduler-simple-inline-scheduler: + +Building a Custom Scheduler: ``simple_inline_scheduler`` +-------------------------------------------------------- + +This section is a worked example, mirroring the structure of +:ref:`simple_then ` for the +scheduler side of the protocol. We'll build a minimal scheduler whose +``schedule()`` produces a sender that completes *synchronously on the +calling thread* — an "inline scheduler". + +stdexec ships a real inline scheduler in +``include/stdexec/__detail/__inline_scheduler.hpp``; we'll call our +walkthrough version ``simple_inline_scheduler`` so it doesn't collide. + +Inline schedulers are useful in tests (synchronous, deterministic) and +as the "default trivial scheduler" when scheduler abstraction is wanted +but actual asynchrony is not. Writing one is also the simplest way to +exercise the *whole* scheduler protocol — there's no queue, no thread +management, no allocator interaction, just the three structural pieces. + +What we're building +^^^^^^^^^^^^^^^^^^^ + +The user-visible API is the standard scheduler shape: + +.. code-block:: cpp + + simple_inline_scheduler sched; + + auto s = stdexec::schedule(sched) | stdexec::then([] { return 42; }); + auto [v] = stdexec::sync_wait(std::move(s)).value(); + // v == 42 + +By the end you'll have a ~45-line scheduler that satisfies the +:ref:`stdexec::scheduler concept ` and works +with :cpp:member:`stdexec::starts_on`, +:cpp:member:`stdexec::continues_on`, :cpp:member:`stdexec::on`, and +everything else that takes a scheduler. + +The shape of a scheduler +^^^^^^^^^^^^^^^^^^^^^^^^ + +A scheduler is a small, value-typed *handle* to an execution context. +The :ref:`scheduler concept ` requires only one +operation — :cpp:member:`stdexec::schedule` — plus plumbing +(equality-comparable, copy-constructible, nothrow-move-constructible). + +So three pieces: + +1. The **scheduler type** itself — a handle, equality-comparable, with + a ``schedule()`` member. +2. The **schedule-sender** — the sender returned by + ``schedule()``. It satisfies :ref:`stdexec::sender + ` and value-completes on the scheduler's + execution resource. +3. The **operation state** — what + :cpp:member:`stdexec::connect`-ing the schedule-sender to a + receiver produces. Unlike the simple_then walkthrough, we have to + write this from scratch — there's no predecessor whose op-state we + can forward to. + +Building it bottom-up — opstate, then sender, then scheduler — makes +each type a complete piece its dependent can refer to. + +Step 1: the operation state +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The op-state holds the receiver and, when started, delivers an empty +value completion synchronously: + +.. code-block:: cpp + + template + struct simple_inline_opstate { + using operation_state_concept = stdexec::operation_state_tag; + + R rcvr_; + + explicit simple_inline_opstate(R rcvr) noexcept + : rcvr_(std::move(rcvr)) {} + + // Operation states must remain at a stable address once started: + simple_inline_opstate(simple_inline_opstate&&) = delete; + + void start() noexcept { + stdexec::set_value(std::move(rcvr_)); + } + }; + +A few things to notice: + +- ``operation_state_concept = operation_state_tag`` opts the type into + the :ref:`operation_state concept `. +- ``start()`` is ``noexcept`` and returns ``void`` — the + :cpp:member:`stdexec::start` dispatch site enforces both with + static asserts. +- ``start()`` calls :cpp:member:`stdexec::set_value` *directly* — + there's nothing async about an inline scheduler. The receiver + observes a completion that happens before ``start`` returns. +- The deleted move constructor is the standard way to assert + immovability. Once an operation state is connected, the framework + (and the receiver inside it) may hold pointers into its storage; + letting it move would dangle those. +- We add an explicit constructor because deleting the move makes the + type non-aggregate — brace-initialization no longer works without a + matching constructor. + +Step 2: the schedule-sender +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The schedule-sender is what ``schedule()`` returns. It's a regular +sender — same shape as in the :ref:`simple_then walkthrough +` — but instead of wrapping a +predecessor, it constructs an ``simple_inline_opstate`` directly: + +.. code-block:: cpp + + struct simple_inline_schedule_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t()>; + + template + auto connect(R rcvr) const noexcept { + return simple_inline_opstate{std::move(rcvr)}; + } + }; + +The interesting parts: + +- The completion signatures declare *one* completion, ``set_value_t()`` + — an empty value completion, no datums. This is the canonical signal + for "we are now on the scheduler's resource; carry on." +- ``connect`` is ``const`` here, not ``&&``. Schedule-senders are + cheap, default-constructible, and stateless (for our scheduler); + copying them is fine and lets ``stdexec::starts_on`` work without + worrying about ownership. + +A more sophisticated scheduler — say, a thread pool — would pass a +pointer/reference to its execution context through the schedule-sender +into the op-state, so that ``start()`` knows *where* to enqueue. + +Step 3: the scheduler +^^^^^^^^^^^^^^^^^^^^^ + +The scheduler is just a handle with a ``schedule()`` member: + +.. code-block:: cpp + + struct simple_inline_scheduler { + auto schedule() const noexcept { + return simple_inline_schedule_sender{}; + } + + bool operator==(simple_inline_scheduler const&) const noexcept = default; + }; + +That's the whole thing. The :ref:`scheduler concept +` requires: + +- ``schedule(s)`` is well-formed and returns a sender. ✓ +- The type is equality-comparable. ✓ (``= default``) +- The type is copy-constructible. ✓ (implicit) +- The type is nothrow-move-constructible. ✓ (implicit) + +Note that :cpp:member:`stdexec::schedule(sched)` is +expression-equivalent to ``sched.schedule()`` — that's how the CPO +dispatches. Notice also that we did *not* need to opt into any concept +explicitly with a tag-alias; the scheduler concept is structural (it +just checks for the ``.schedule()`` member and the value-semantics +plumbing). + +**On equality.** + +Two schedulers compare equal *iff they refer to the same execution +resource*. For our inline scheduler this is uninteresting: there's +only one "calling thread" in any meaningful sense, so any two +``simple_inline_scheduler`` instances are equivalent. ``= default`` +gives us that. + +For a thread-pool scheduler, equality would compare the underlying +pool pointer — adaptors like ``continues_on`` use this to elide +redundant scheduler hops (if the target is the same as the current, +the hop is a no-op). + +Putting it all together +^^^^^^^^^^^^^^^^^^^^^^^ + +Here is the complete compilable example: + +.. code-block:: cpp + + #include + #include + #include + + // ---------- Operation state ----------------------------------------- + + template + struct simple_inline_opstate { + using operation_state_concept = stdexec::operation_state_tag; + + R rcvr_; + + explicit simple_inline_opstate(R rcvr) noexcept + : rcvr_(std::move(rcvr)) {} + + simple_inline_opstate(simple_inline_opstate&&) = delete; + + void start() noexcept { + stdexec::set_value(std::move(rcvr_)); + } + }; + + // ---------- Schedule-sender ----------------------------------------- + + struct simple_inline_schedule_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = stdexec::completion_signatures< + stdexec::set_value_t()>; + + template + auto connect(R rcvr) const noexcept { + return simple_inline_opstate{std::move(rcvr)}; + } + }; + + // ---------- Scheduler ----------------------------------------------- + + struct simple_inline_scheduler { + auto schedule() const noexcept { + return simple_inline_schedule_sender{}; + } + + bool operator==(simple_inline_scheduler const&) const noexcept = default; + }; + + // ---------- Try it out ---------------------------------------------- + + int main() { + // Use schedule() directly: + auto s = stdexec::schedule(simple_inline_scheduler{}) + | stdexec::then([] { return 42; }); + auto [v] = stdexec::sync_wait(std::move(s)).value(); + assert(v == 42); + + // Use starts_on to run an entire pipeline on the scheduler: + auto t = stdexec::starts_on( + simple_inline_scheduler{}, + stdexec::just(21) | stdexec::then([](int x) { return x * 2; })); + auto [w] = stdexec::sync_wait(std::move(t)).value(); + assert(w == 42); + + // The scheduler concept is satisfied: + static_assert(stdexec::scheduler); + } + +That's a complete scheduler. Anywhere an stdexec scheduler is accepted +— :cpp:member:`stdexec::starts_on`, +:cpp:member:`stdexec::continues_on`, :cpp:member:`stdexec::on`, +:cpp:member:`stdexec::schedule_from`, or even as the value of a +``get_scheduler`` query on a receiver's environment — this scheduler +just works. + +Going further +^^^^^^^^^^^^^ + +The inline scheduler is the minimal possible scheduler. Realistic +schedulers add three things, each a relatively small extension: + +**A queue and a context.** +A *deferred* scheduler — one whose ``schedule()`` doesn't complete +synchronously — holds a pointer or reference to an execution context +(thread pool, run loop, event loop). The schedule-sender carries +that context pointer through to the operation state, and ``start()`` +enqueues the op-state for later execution rather than calling +``set_value`` immediately. The op-state typically derives from a +linked-list node so the queue can intrusively link it. stdexec's +``stdexec::run_loop`` (in +``include/stdexec/__detail/__run_loop.hpp``) is a good first reference +— it's a single-threaded run-loop scheduler in ~250 lines. + +**Stop-token observation.** +A real scheduler reads the receiver's environment for a stop token +(via :cpp:member:`stdexec::get_env` followed by +``stdexec::get_stop_token``), registers a callback on it, and +completes with :cpp:member:`stdexec::set_stopped` if cancellation is +requested before the operation gets to run. Add a +``set_stopped_t()`` to the schedule-sender's completion signatures and +a stop-callback member to the operation state. + +**Allocator handling.** +Schedulers that allocate per-operation state (most of them) should +consult ``stdexec::get_allocator`` on the receiver's environment so +allocations honor the caller's preferences. Falling back to +``std::allocator`` is fine for the default. + +**Domain customization.** +A scheduler can additionally publish a *domain* tag in its +environment, which lets it intercept and rewrite sender expressions +specifically targeted at it — e.g. the GPU scheduler taking over a +``then`` chain so the lambdas run on-device. This is the topic of +the (still-to-be-written) *Customizing stdexec's algorithms* section +below. + +Each of these extensions composes with the rest — you can add them +one at a time without redesigning the basic structure. + +.. _customizing-stdexec-s-algorithms-via-domains: + +Customizing stdexec's algorithms via domains +-------------------------------------------- + +The previous walkthroughs taught a scheduler how to *host* execution, +but they left the algorithms themselves untouched. Calling +:cpp:member:`stdexec::then` on a sender hosted by your scheduler still +runs the user's lambda through plain ``std::invoke`` — on the CPU, in +the receiver-completion thread. + +For some execution contexts that is wrong. A GPU scheduler wants +:cpp:member:`stdexec::then` lambdas to execute *on the device*; a tracing +scheduler wants every algorithm wrapped with span-recording code; a +fault-injection scheduler wants every algorithm interceptible from a +test harness. None of these can be expressed by writing a different +scheduler — the *algorithms* themselves need to know they're operating +in a special context. + +stdexec exposes this through *domains*. A scheduler publishes a domain +through its environment; the framework consults that domain at +``connect`` time, giving it a chance to rewrite any sender expression +in the pipeline before the algorithms see it. This is the same hook +nvexec uses to make standard algorithms compile to CUDA kernels. + +What a domain is +^^^^^^^^^^^^^^^^ + +A domain is just a *tag type* — usually empty — with a particular +shape of customization member: + +.. code-block:: cpp + + struct my_domain { + template + static auto transform_sender(OpTag, Sndr&& sndr, Env const&) + /* -> some-new-sender-or-the-same-sender */; + }; + +The framework calls ``my_domain{}.transform_sender(tag, sndr, env)`` and +uses the *returned* sender — whatever that is — in place of the +original. Returning the input unchanged is a no-op customization; +returning a structurally different sender is how a domain *rewrites* +the pipeline. + +The ``OpTag`` argument is the *kind* of customization being requested: + +- ``stdexec::set_value_t`` — the sender will be connected on its + *completion* domain (the domain advertised by the predecessor). + This is what GPU schedulers hook to take over algorithms that + produced data on the GPU. +- ``stdexec::start_t`` — the sender will be started in the *current* + domain (the domain in the receiver's environment). This is the path + used to react to the consumer side: "I am about to start on this + domain, transform me first." + +A given domain may handle both. Most real-world domains primarily care +about ``set_value_t``. + +How the framework consults a domain +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The hook lives inside :cpp:member:`stdexec::connect`. When you write + +.. code-block:: cpp + + auto op = stdexec::connect(sndr, rcvr); + +the framework expands roughly to + +.. code-block:: cpp + + // pseudo-code + auto env = stdexec::get_env(rcvr); + auto completing_domain = /* read get_completion_domain + from sndr's attributes */; + auto starting_domain = /* read get_domain from env (or default) */; + + auto sndr1 = completing_domain.transform_sender(set_value_t{}, sndr, env); + auto sndr2 = starting_domain .transform_sender(start_t{}, sndr1, env); + + auto op = sndr2.connect(rcvr); // (or static_connect, awaitable, ...) + +Each ``transform_sender`` call delegates by default to a tag-type hook +on the sender (more below). If the sender's tag type does not provide +``transform_sender``, the default behavior is to return the sender +unchanged. + +This is a two-phase model: + +1. **Completing-domain transformation** — driven by the sender's + advertised completion domain. Used by algorithms like + :cpp:member:`stdexec::on`, whose ``transform_sender`` rewrites + ``on(sch, sndr)`` into ``continues_on(starts_on(sch, sndr), + orig_sch)``. +2. **Starting-domain transformation** — driven by the receiver's + environment. Used to allow the *eventual consumer* to inject + transformations. + +Both happen at the same point (inside ``connect``) — there is no +"early" customization before ``connect`` any more. (Older versions of +the design had an "early" form, performed at sender construction +time and without an environment; it has since been removed.) + +Two ways to participate +^^^^^^^^^^^^^^^^^^^^^^^ + +There are two customization paths, depending on whether you're writing +an algorithm or a scheduler. + +**Tag-type customization** — for sender adaptor authors. Define a +``static transform_sender`` member on the sender's *tag type* (the +``foo_t`` of your ``foo``-adaptor) — the default domain's +``transform_sender`` finds it and forwards to it. This is what +``stdexec::on_t::transform_sender`` does to expand +``on(sch, sndr)`` into a combination of ``starts_on`` and +``continues_on``: + +.. code-block:: cpp + + // Lightly paraphrased from include/stdexec/__detail/__on.hpp. + struct on_t { + // ... operator() overloads ... + + template + static auto transform_sender(set_value_t, Sender&& sndr, Env&& env) + { + auto& [tag, data, child] = sndr; // destructure the s-expression + return /* continues_on(starts_on(data, child), orig_sch) */; + } + }; + +Tag-type customizations are how an adaptor implements its semantics +without writing the operation state by hand. They run regardless of +which domain is active — the rewrite is universal. + +**Domain-level customization** — for scheduler authors. Define a +custom domain type with a ``transform_sender`` member that intercepts +algorithms in *its* execution context. Publish the domain through the +scheduler's environment. The framework will route every sender flowing +through a pipeline anchored on your scheduler through your domain's +``transform_sender`` first. + +This is the GPU-scheduler story: nvexec's domain has a +``transform_sender`` that recognizes ``then_t``, ``bulk_t``, +``when_all_t`` etc. coming from its own schedule-sender and rewrites +them into CUDA-kernel-launching senders. + +Worked example: a scheduler with a noticing domain +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For pedagogy, we'll skip the rewriting payload (it requires reaching +into a specific sender's structure, which varies per algorithm) and +focus on the *plumbing*. Our domain will simply *count* how many times +it is consulted at ``connect`` time — proof that the hook is live. + +The structure is identical to the +:ref:`simple_inline_scheduler walkthrough +`, with three +additions: + +1. A ``my_domain`` type with a ``transform_sender`` member. +2. The schedule-sender's attributes (``attrs_t``) advertise both + ``get_completion_scheduler`` (returning the scheduler + itself) and ``get_completion_domain`` (returning + ``my_domain``). +3. The scheduler also answers ``get_completion_scheduler`` and + ``get_completion_domain`` — the framework cross-checks that the + scheduler's domain matches the schedule-sender's advertised domain + and refuses to compile if they disagree. + +.. code-block:: cpp + + #include + #include + #include + #include + + // Module-level state so we can observe whether our domain was consulted. + static std::atomic g_transform_count{0}; + + // ---------- The domain ---------------------------------------------- + + struct my_domain { + // The framework calls this at connect time. We don't rewrite the + // sender — we just record that we were consulted, then forward. + template + static auto transform_sender(OpTag, Sndr&& sndr, Env const&) { + g_transform_count.fetch_add(1, std::memory_order_relaxed); + return static_cast(sndr); + } + }; + + // ---------- A scheduler that publishes my_domain -------------------- + + template + struct my_sched_opstate { + using operation_state_concept = stdexec::operation_state_tag; + R rcvr_; + + explicit my_sched_opstate(R rcvr) noexcept : rcvr_(std::move(rcvr)) {} + my_sched_opstate(my_sched_opstate&&) = delete; + + void start() noexcept { stdexec::set_value(std::move(rcvr_)); } + }; + + struct my_scheduler; // forward + + struct my_schedule_sender { + using sender_concept = stdexec::sender_tag; + using completion_signatures = + stdexec::completion_signatures; + + struct attrs_t { + auto query(stdexec::get_completion_scheduler_t) + const noexcept -> my_scheduler; + auto query(stdexec::get_completion_domain_t) + const noexcept { return my_domain{}; } + }; + + auto get_env() const noexcept { return attrs_t{}; } + + template + auto connect(R rcvr) const noexcept { + return my_sched_opstate{std::move(rcvr)}; + } + }; + + struct my_scheduler { + auto schedule() const noexcept { return my_schedule_sender{}; } + + // A scheduler is its own completion scheduler, and claims my_domain + // as the domain on which it completes. + auto query(stdexec::get_completion_scheduler_t) + const noexcept { return *this; } + auto query(stdexec::get_completion_domain_t) + const noexcept { return my_domain{}; } + + bool operator==(my_scheduler const&) const noexcept = default; + }; + + inline auto my_schedule_sender::attrs_t::query( + stdexec::get_completion_scheduler_t) const noexcept + -> my_scheduler { + return {}; + } + + // ---------- Try it out ---------------------------------------------- + + int main() { + static_assert(stdexec::scheduler); + + auto count_before = g_transform_count.load(); + + auto pipeline = stdexec::schedule(my_scheduler{}) + | stdexec::then([] { return 42; }); + auto [v] = stdexec::sync_wait(std::move(pipeline)).value(); + assert(v == 42); + + // The domain was consulted at least once at connect time: + assert(g_transform_count.load() > count_before); + } + +Running this and observing ``g_transform_count > 0`` after +:cpp:member:`stdexec::sync_wait` returns is the empirical proof that +the domain hook is wired up. From there, replacing the +``return static_cast(sndr);`` line with an actual rewrite — +inspecting the sender's tag type and rebuilding it as something +different — is what turns this scaffolding into a real customization. + +When and what to rewrite +^^^^^^^^^^^^^^^^^^^^^^^^ + +The body of a real ``transform_sender`` typically inspects +``stdexec::tag_of_t`` (or equivalently the result of pattern-matching +on the sender's tag) and rewrites only the senders whose tags are +"interesting" to this domain. Everything else is forwarded unchanged. + +The canonical pattern is a chain of ``if constexpr`` branches: + +.. code-block:: cpp + + template + static auto transform_sender(OpTag op, Sndr&& sndr, Env const& env) { + using tag = stdexec::tag_of_t; + if constexpr (std::same_as) { + return /* my-domain's version of then(sndr, fn) */; + } else if constexpr (std::same_as) { + return /* my-domain's version of bulk(...) */; + } else { + return static_cast(sndr); // pass through + } + } + +Each rewrite typically destructures the original sender via the s-expression +machinery, extracts its data and child(ren), and rebuilds an equivalent +sender that runs on the domain's resource. nvexec's ``stream_domain`` does +this for every CUDA-kernel-compatible algorithm. + +Pitfalls +^^^^^^^^ + +**Consistency between scheduler and schedule-sender attrs.** +If both the scheduler and the schedule-sender's attrs advertise a +domain, the framework cross-checks them at compile time and emits the +static assertion ``"the sender claims to complete on a domain that is +not the domain of its completion scheduler"`` if they disagree. Keep +them in lock-step. + +**Don't capture the environment.** +``transform_sender`` is given the environment by const-reference; +rewriting it into a different sender that *captures* the environment +would dangle. Use the environment only for compile-time decisions +(constraint checks, alternative-selection). + +**``transform_sender`` runs at connect time, once per call.** +It is not invoked at sender construction time, and it doesn't see the +op-state. If your rewrite needs runtime state, smuggle it through the +*sender itself* (as a data member of the type your rewrite produces). + +Topics still to be written +^^^^^^^^^^^^^^^^^^^^^^^^^^ - * - **CPO** - - **Description** - * - :cpp:member:`stdexec::connect` - - Connects a sender to a receiver resulting in an operation state. - * - :cpp:member:`stdexec::start` - - Starts the operation. - * - :cpp:member:`stdexec::set_value` - - Called by the operation state to deliver a value to the receiver. - * - :cpp:member:`stdexec::set_error` - - Called by the operation state to deliver an error to the receiver. - * - :cpp:member:`stdexec::set_stopped` - - Called by the operation state to indicate that the operation was stopped. - * - :cpp:member:`stdexec::get_env` - - Retrieves the environment from a receiver. - - - - -* Receivers -* Custom Algorithms -* Custom Schedulers -* Customizing ``stdexec``'s algorithms - * Domains - * Early algorithm customization - * Late algorithm customization +* A worked example of an actual rewriting domain (e.g. a tracing + ``then`` that wraps the user's callable). diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index 1ba8cadce..50d1a9660 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -16,27 +16,306 @@ Reference .. toctree:: :maxdepth: 2 +.. _ref-section-concepts: + Concepts -------- -TODO: Add concepts section +The stdexec API is structured around a small set of foundational concepts. +Most sender adaptors and consumers express their requirements in terms of +these concepts, so understanding them — and which one to reach for in +which situation — pays off across the rest of the reference. + +The concepts fall into three layers: + +- **Sender side:** :ref:`ref-concept-sender`, :ref:`ref-concept-sender_in`, + :ref:`ref-concept-sender_to`. A *sender* is the basic unit of + composition — a value that *describes* (but does not yet execute) an + async computation. +- **Receiver / operation-state side:** + :ref:`ref-concept-receiver`, :ref:`ref-concept-receiver_of`, + :ref:`ref-concept-operation_state`. These describe the *consumer* + half of a sender/receiver pair — the destination of completion signals + and the running operation that delivers them. +- **Context side:** :ref:`ref-concept-scheduler`, + :ref:`ref-concept-scope_token`, :ref:`ref-concept-scope_association`. + These describe how work is dispatched onto execution resources and how + its lifetime is tracked. + +Sender concepts +~~~~~~~~~~~~~~~ + +.. _ref-concept-sender: + +``sender`` +^^^^^^^^^^ + +.. doxygenconcept:: stdexec::sender + +.. doxygenstruct:: stdexec::sender_tag + +.. doxygenvariable:: stdexec::enable_sender + +.. _ref-concept-sender_in: + +``sender_in`` +^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::sender_in + +.. _ref-concept-sender_to: + +``sender_to`` +^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::sender_to + +Receiver and operation-state concepts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _ref-concept-receiver: + +``receiver`` +^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::receiver + +.. doxygenstruct:: stdexec::receiver_tag + +.. _ref-concept-receiver_of: + +``receiver_of`` +^^^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::receiver_of + +.. _ref-concept-operation_state: + +``operation_state`` +^^^^^^^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::operation_state + +.. doxygenstruct:: stdexec::operation_state_tag + +Context concepts +~~~~~~~~~~~~~~~~ + +.. _ref-concept-scheduler: + +``scheduler`` +^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::scheduler + +.. _ref-concept-scope_token: + +``scope_token`` +^^^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::scope_token + +.. _ref-concept-scope_association: + +``scope_association`` +^^^^^^^^^^^^^^^^^^^^^ + +.. doxygenconcept:: stdexec::scope_association + +.. _ref-section-cpos: + +Core Customization Points +------------------------- + +The customization points listed here are the *defining operations* of the +sender model. They are what every sender, receiver, and operation state +type must support (each in its own way) to participate in the protocol. +Most user code never calls these directly — sender adaptors and consumers +do — but anyone *writing* a new sender, receiver, or scheduler will +implement one or more of these. + +The CPOs fall into three layers: + +- **Sender-side:** :ref:`connect `, + :ref:`get_completion_signatures `, + :ref:`get_env `. These describe how a sender exposes + its computation and attributes to the framework. +- **Operation-state-side:** :ref:`start `. The trigger + that turns a connected sender into a running operation. +- **Receiver-side:** :ref:`set_value `, + :ref:`set_error `, + :ref:`set_stopped `, + :ref:`get_env `. These describe how an operation + state delivers a completion to its receiver and queries the receiver's + environment. + +See the :ref:`Developer's Guide ` for a +narrative walkthrough of how these fit together when writing a new +sender adaptor. + +Sender-side +~~~~~~~~~~~ + +.. _ref-cpo-connect: + +``connect`` +^^^^^^^^^^^ + +.. doxygenstruct:: stdexec::connect_t + :members: + +.. doxygenvariable:: stdexec::connect + +.. _ref-cpo-get_completion_signatures: + +``get_completion_signatures`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Unlike the other entries in this section, ``get_completion_signatures`` +is a *function template* (not a CPO instance), so it has no underlying +struct. The two forms — environment-free and environment-dependent — +share documentation: + +.. doxygenfile:: __get_completion_signatures.hpp + :sections: briefdescription detaileddescription + +Operation-state-side +~~~~~~~~~~~~~~~~~~~~ + +.. _ref-cpo-start: + +``start`` +^^^^^^^^^ + +.. doxygenstruct:: stdexec::start_t + :members: + +.. doxygenvariable:: stdexec::start + +Receiver-side +~~~~~~~~~~~~~ + +.. _ref-cpo-set_value: + +``set_value`` +^^^^^^^^^^^^^ + +.. doxygenstruct:: stdexec::set_value_t + :members: + +.. doxygenvariable:: stdexec::set_value + +.. _ref-cpo-set_error: + +``set_error`` +^^^^^^^^^^^^^ + +.. doxygenstruct:: stdexec::set_error_t + :members: + +.. doxygenvariable:: stdexec::set_error + +.. _ref-cpo-set_stopped: + +``set_stopped`` +^^^^^^^^^^^^^^^ + +.. doxygenstruct:: stdexec::set_stopped_t + :members: + +.. doxygenvariable:: stdexec::set_stopped + +.. _ref-cpo-get_env: + +``get_env`` +^^^^^^^^^^^ + +.. doxygenstruct:: stdexec::get_env_t + :members: + +.. doxygenvariable:: stdexec::get_env Sender Factories ---------------- -TODO: more sender factory algorithms +A *sender factory* is an algorithm that produces a sender from non-sender +inputs (values, an error, a scheduler, an environment query). Factories +sit at the *head* of a sender pipeline. -.. doxygenvariable:: stdexec::schedule +.. _ref-just: + +``just`` — produce a sender from values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender that synchronously completes with the given values on +the *value* channel. The canonical way to inject literal values into a +sender pipeline. See :ref:`UserGuide_just` for an approachable +introduction with worked examples. + +.. doxygenstruct:: stdexec::just_t + :members: .. doxygenvariable:: stdexec::just +.. _ref-just_error: + +``just_error`` — produce a sender from an error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender that synchronously completes with the given error on +the *error* channel. Mostly useful for testing error-handling adaptors. + +.. doxygenstruct:: stdexec::just_error_t + :members: + .. doxygenvariable:: stdexec::just_error +.. _ref-just_stopped: + +``just_stopped`` — produce a stopped sender +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender that synchronously completes on the *stopped* channel. +Mostly useful for testing cancellation-handling adaptors. + +.. doxygenstruct:: stdexec::just_stopped_t + :members: + .. doxygenvariable:: stdexec::just_stopped +.. _ref-read_env: + +``read_env`` — produce a sender from an environment query +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender whose value completion is the result of querying the +connected receiver's environment with a given query CPO. It is the +primitive behind the standard ``get_stop_token()``, ``get_allocator()``, +``get_scheduler()`` helpers. See :ref:`UserGuide_read_env` for an +approachable introduction. + .. doxygenvariable:: stdexec::read_env -.. .. doxygenstruct:: experimental::execution::just_from_t +.. _ref-schedule: + +``schedule`` — produce a sender from a scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender that, when connected and started, value-completes +from the context of the given scheduler. It is the bridge between the +scheduler and sender abstractions, and the way to begin a pipeline that +must run on a specific execution context. See :ref:`UserGuide_schedule` +for an approachable introduction. + +.. doxygenstruct:: stdexec::schedule_t + :members: + +.. doxygenvariable:: stdexec::schedule + +.. _ref-just_from: + +``just_from`` (experimental) — like ``just`` but value-producing via a function +""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .. doxygenvariable:: experimental::execution::just_from @@ -49,36 +328,433 @@ Sender Adaptors TODO: More sender adaptor algorithms +.. _ref-then: + +``then`` — apply a function to the value channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Transforms a predecessor sender's value completion by invoking a callable +with the values it produces. See :ref:`UserGuide_then` for an approachable +introduction with worked examples; the complete reference (including +completion-signature transformation rules, exception behavior, and the +``operator()`` overloads) follows. + .. doxygenstruct:: stdexec::then_t + :members: .. doxygenvariable:: stdexec::then +.. _ref-upon_error: + +``upon_error`` — handle the error channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Handles a predecessor sender's error completion by invoking a callable on +the error datum and delivering the result as a *value* completion — the +canonical way to recover from an error and continue the pipeline. See +:ref:`UserGuide_upon_error` for an approachable introduction with worked +examples. + +.. doxygenstruct:: stdexec::upon_error_t + :members: + +.. doxygenvariable:: stdexec::upon_error + +.. _ref-upon_stopped: + +``upon_stopped`` — handle the stopped channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Handles a predecessor sender's stopped completion by invoking a nullary +callable and delivering its return value as a *value* completion — the +canonical way to recover from cancellation. See :ref:`UserGuide_upon_stopped` +for an approachable introduction with worked examples. + +.. doxygenstruct:: stdexec::upon_stopped_t + :members: + +.. doxygenvariable:: stdexec::upon_stopped + +.. _ref-let_value: + +``let_value`` — chain a sender-returning function on the value channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Chains a *sender-returning* function onto a predecessor's value completion. +The returned sender is connected and started, and its completions become +the completions of the overall pipeline. This is the way to launch another +asynchronous operation based on a predecessor's values. See +:ref:`UserGuide_let_value` for an approachable introduction with worked +examples. + +.. doxygenstruct:: stdexec::let_value_t + +.. doxygenvariable:: stdexec::let_value + +.. _ref-let_error: + +``let_error`` — chain a sender-returning function on the error channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Chains a *sender-returning* function onto a predecessor's error completion +— the way to launch another asynchronous operation to recover from an +error. + +.. doxygenstruct:: stdexec::let_error_t + +.. doxygenvariable:: stdexec::let_error + +.. _ref-let_stopped: + +``let_stopped`` — chain a sender-returning function on the stopped channel +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Chains a *sender-returning* nullary function onto a predecessor's stopped +completion — the way to launch another asynchronous operation to recover +from cancellation. + +.. doxygenstruct:: stdexec::let_stopped_t + +.. doxygenvariable:: stdexec::let_stopped + +Scheduling adaptors +~~~~~~~~~~~~~~~~~~~ + +These adaptors move work between execution contexts. ``starts_on`` begins a +sender on a new scheduler; ``continues_on`` transfers execution to a new +scheduler after a sender completes; ``on`` runs work on a different +scheduler and then returns to where it started. See +:ref:`UserGuide_scheduling_adaptors` for a side-by-side comparison. + +.. _ref-starts_on: + +``starts_on`` — run a sender on a scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender that runs an input sender starting on a given +scheduler's execution resource. The completion is delivered on that +same resource (no round-trip back). + +.. doxygenstruct:: stdexec::starts_on_t + :members: + .. doxygenvariable:: stdexec::starts_on +.. _ref-continues_on: + +``continues_on`` — transfer to a scheduler after completion +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Produces a sender that runs the input sender to completion, then transfers +execution to a given scheduler's resource before delivering the +completion downstream. Anything chained after ``continues_on`` runs on +the new scheduler. + +.. doxygenstruct:: stdexec::continues_on_t + :members: + .. doxygenvariable:: stdexec::continues_on +.. _ref-on: + +``on`` — run on a scheduler and return to the original +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The "go there, do work, come back" adaptor. Two forms: +``on(sched, sndr)`` runs ``sndr`` on ``sched`` and returns to the start +scheduler; ``on(sndr, sched, closure)`` (and its pipe form +``sndr | on(sched, closure)``) hops to ``sched`` for an inserted +closure then hops back. See :ref:`UserGuide_on` for guidance on when to +reach for which form. + +.. doxygenstruct:: stdexec::on_t + :members: + .. doxygenvariable:: stdexec::on +Composition adaptors +~~~~~~~~~~~~~~~~~~~~ + +These adaptors combine multiple senders into one. They are the building +blocks for parallel and fan-out patterns. + +.. _ref-when_all: + +``when_all`` — run senders concurrently and concatenate values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Takes one or more senders and produces a sender that, when started, runs +all of them concurrently and completes when every input has completed. +The resulting value-completion is the *concatenation* of every input's +value datums. If any input fails or is stopped, the others are +cancelled and the result is the first error/stopped completion observed. +Each input must have exactly one value-completion shape; for senders +that can succeed in more than one way, see :ref:`ref-when_all_with_variant`. + +.. doxygenstruct:: stdexec::when_all_t + :members: + .. doxygenvariable:: stdexec::when_all -.. doxygenvariable:: stdexec::let_value +.. _ref-when_all_with_variant: -.. doxygenvariable:: stdexec::let_error +``when_all_with_variant`` — like ``when_all`` for multi-completion senders +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. doxygenvariable:: stdexec::let_stopped +Like ``when_all``, but lifts the "exactly one value-completion per +input" restriction by wrapping each input in +:cpp:member:`stdexec::into_variant`. Each output value-completion +position is a ``std::variant, ...>`` of that input's +possible shapes. + +.. doxygenstruct:: stdexec::when_all_with_variant_t + :members: + +.. doxygenvariable:: stdexec::when_all_with_variant + +.. _ref-into_variant: + +``into_variant`` — collapse multi-completion senders into one +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Reshapes a sender that may value-complete in more than one way into a +sender that always value-completes with a single +``std::variant``-of-tuples datum. It is the building block behind +:ref:`when_all_with_variant ` and +:ref:`sync_wait_with_variant `, +and is occasionally useful on its own when a downstream algorithm +requires the single-value-completion form. + +.. doxygenstruct:: stdexec::into_variant_t + :members: + +.. doxygenvariable:: stdexec::into_variant + +``transfer_when_all`` *(deprecated)* +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. admonition:: Deprecated + :class: warning + + This adaptor is not part of the C++26 working draft and is retained + only for backwards compatibility. Write + ``when_all(sndrs...) | continues_on(sch)`` instead — the behavior is + identical. + +.. doxygenstruct:: stdexec::transfer_when_all_t + :members: + +.. doxygenvariable:: stdexec::transfer_when_all + +``transfer_when_all_with_variant`` *(deprecated)* +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. admonition:: Deprecated + :class: warning + + This adaptor is not part of the C++26 working draft and is retained + only for backwards compatibility. Write + ``when_all_with_variant(sndrs...) | continues_on(sch)`` instead. + +.. doxygenstruct:: stdexec::transfer_when_all_with_variant_t + :members: + +.. doxygenvariable:: stdexec::transfer_when_all_with_variant + +Parallel-loop adaptors +~~~~~~~~~~~~~~~~~~~~~~ + +The ``bulk`` family invokes a callable over an integer index space, +under a given execution policy. They are the parallel-loop primitives +of the sender model — the entry point for GPU/parallel-scheduler +customizations to take over. -.. .. doxygenvariable:: stdexec::write_env +.. _ref-bulk: + +``bulk`` — apply a function to each index +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Invokes ``f(i, vs...)`` for every ``i`` in ``[0, shape)`` under the +given execution policy. Lowers to ``bulk_chunked`` internally so that +domain customizations of ``bulk_chunked`` apply transparently. See +:ref:`UserGuide_bulk` for a worked example and policy discussion. + +.. doxygenstruct:: stdexec::bulk_t + +.. doxygenvariable:: stdexec::bulk + +.. _ref-bulk_chunked: + +``bulk_chunked`` — apply a function per chunk of indices +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Invokes ``f(begin, end, vs...)`` for chunks of the index space. +The implementation may split into any number of chunks (one, +shape, anything between). + +.. doxygenstruct:: stdexec::bulk_chunked_t + +.. doxygenvariable:: stdexec::bulk_chunked + +.. _ref-bulk_unchunked: + +``bulk_unchunked`` — apply a function per index, no chunking allowed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Like ``bulk`` but forbids the implementation from combining multiple +indices into a single call. Use when per-iteration state (thread-local +accumulators, per-index hardware resources) prevents batching. + +.. doxygenstruct:: stdexec::bulk_unchunked_t + +.. doxygenvariable:: stdexec::bulk_unchunked + +Stopped-channel translator adaptors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These adaptors don't change the *behavior* of a pipeline; they +translate one completion channel into another, exposing a friendlier +shape to downstream code. + +.. _ref-stopped_as_error: + +``stopped_as_error`` — translate stopped into an error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Converts a ``set_stopped`` completion into a ``set_error`` completion +carrying a caller-supplied error datum. The resulting sender no longer +has a stopped channel. + +.. doxygenstruct:: stdexec::stopped_as_error_t + :members: + +.. doxygenvariable:: stdexec::stopped_as_error + +.. _ref-stopped_as_optional: + +``stopped_as_optional`` — translate stopped into a value-channel ``nullopt`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Converts a ``set_stopped`` completion into a value-channel +``std::optional{std::nullopt}``, wrapping the predecessor's value +in ``std::optional``. Requires the predecessor to have exactly one +value completion with one argument. + +.. doxygenstruct:: stdexec::stopped_as_optional_t + :members: + +.. doxygenvariable:: stdexec::stopped_as_optional + +Environment adaptors +~~~~~~~~~~~~~~~~~~~~ + +.. _ref-write_env: + +``write_env`` — inject values into the environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Augments the environment seen by a predecessor sender with +additional queries. The inverse of :ref:`read_env `: +``read_env`` exposes environment values into the value channel, +``write_env`` injects environment values into a child sender's +environment. + +.. doxygenvariable:: stdexec::write_env Sender Consumers ---------------- -TODO: More sender consumer algorithms +A *sender consumer* takes a sender, connects it to a receiver, and starts +the resulting operation. Consumers sit at the *tail* of a sender pipeline +— they are the point at which asynchronous work actually runs. They fall +into two broad families: + +- **Synchronous waiters** (:ref:`sync_wait `, + :ref:`sync_wait_with_variant `) block the + calling thread until the pipeline completes and return the result. +- **Eager launchers** (:ref:`start_detached `, + :ref:`spawn `, :ref:`spawn_future `) + start the pipeline immediately and either discard the result + (:ref:`spawn ` and :ref:`start_detached `) + or expose it as a sender that observes the running operation + (:ref:`spawn_future `). + +See :ref:`UserGuide_sender_consumers` for a side-by-side comparison and +guidance on which consumer to reach for. + +.. _ref-sync_wait: + +``sync_wait`` — block until the sender completes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Synchronously waits for a single-value-completion sender to complete on +the calling thread. Returns an engaged ``std::optional>`` +on success, an empty optional on stopped, and throws on error. + +.. doxygenstruct:: stdexec::sync_wait_t + :members: .. doxygenvariable:: stdexec::sync_wait +.. _ref-sync_wait_with_variant: + +``sync_wait_with_variant`` — block until a multi-completion sender completes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Like ``sync_wait`` but for senders that may complete with more than one +value-completion shape. Returns an engaged +``std::optional...>>`` on success. + +.. doxygenstruct:: stdexec::sync_wait_with_variant_t + :members: + +.. doxygenvariable:: stdexec::sync_wait_with_variant + +.. _ref-start_detached: + +``start_detached`` *(extension)* — fire and forget +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Eagerly starts a sender and discards its result. The operation state is +heap-allocated and cleans itself up on completion. **stdexec extension** +— not part of the C++26 working draft. For the standardized +scope-tracked equivalent, see :ref:`spawn `. + +.. doxygenstruct:: experimental::execution::start_detached_t + :members: + .. doxygenvariable:: experimental::execution::start_detached +.. _ref-spawn: + +``spawn`` — fire and forget into an async scope +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Eagerly starts a sender and ties its lifetime to a given async scope. +The argument sender must not be able to complete with ``set_error``. +The standardized way to launch fire-and-forget work whose lifetime +should be tracked. + +.. doxygenstruct:: stdexec::spawn_t + :members: + +.. doxygenvariable:: stdexec::spawn + +.. _ref-spawn_future: + +``spawn_future`` — fire and observe via a sender +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Like ``spawn`` but additionally returns a sender that completes when the +spawned operation completes. The returned sender is a *one-shot +observer* of work that is already running, not a re-runnable handle. + +.. doxygenstruct:: stdexec::spawn_future_t + :members: + +.. doxygenvariable:: stdexec::spawn_future + Utilities --------- diff --git a/docs/source/user/index.rst b/docs/source/user/index.rst index 96d63c63b..ae8778470 100644 --- a/docs/source/user/index.rst +++ b/docs/source/user/index.rst @@ -156,11 +156,1048 @@ Here is an example of using sender algorithms to create a simple async pipeline: auto [result] = stdexec::sync_wait(std::move(pipeline)).value(); +📖 Algorithms in Depth +---------------------- + +This section gives a more approachable, example-driven introduction to the +individual sender algorithms. For exhaustive technical reference — including +template parameters, completion-signature transformation rules, and exception +behavior — see the :ref:`Reference` section. + +Sender factories +~~~~~~~~~~~~~~~~ + +Factories produce a sender from non-sender inputs. They sit at the *head* +of a pipeline. + +.. _UserGuide_just: + +``just`` — inject literal values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::just` is the simplest sender factory. You give it +zero or more values; you get back a sender that, when started, immediately +delivers those values to its receiver as a value completion. No context +transition, no asynchrony — just a synchronous handoff of values into the +sender world. + +.. code-block:: cpp + + auto sndr = stdexec::just(21) + | stdexec::then([](int x) { return x * 2; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 + +``just`` can take any number of values, including zero: + +.. code-block:: cpp + + auto s0 = stdexec::just(); // value-completes with no datums + auto s2 = stdexec::just(1, "hello"); // value-completes with int, string + +Use ``just`` whenever you need to start a pipeline with a fixed value, or +to feed test data into an algorithm during unit tests. + +.. _UserGuide_just_error: + +``just_error`` — inject a literal error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::just_error` is to the error channel what ``just`` +is to the value channel: it produces a sender that immediately completes +with the given error. + +.. code-block:: cpp + + auto sndr = stdexec::just_error(std::error_code{ENOENT, std::system_category()}) + | stdexec::upon_error([](std::error_code) { return -1; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == -1 + +This is mostly useful in tests, where you want to drive an error-handling +adaptor (:cpp:member:`stdexec::upon_error`, :cpp:member:`stdexec::let_error`) +without having to construct a sender that actually fails. + +.. _UserGuide_just_stopped: + +``just_stopped`` — inject a literal cancellation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::just_stopped` produces a sender that immediately +completes via ``set_stopped`` (no datums — the stopped channel carries +none). + +.. code-block:: cpp + + auto sndr = stdexec::just_stopped() + | stdexec::upon_stopped([] { return 42; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 + +Useful, like ``just_error``, primarily for testing cancellation-handling +adaptors. + +.. _UserGuide_read_env: + +``read_env`` — read a value from the receiver's environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::read_env` lets a pipeline inspect properties of its +*receiver* — the receiver's stop token, allocator, preferred scheduler, +and anything else the environment exposes. You give it a *query CPO* +(``stdexec::get_stop_token``, ``stdexec::get_scheduler``, …) and get +back a sender that, when started, evaluates that query against the +connected receiver's environment and delivers the result as a value. + +.. code-block:: cpp + + auto sndr = + stdexec::read_env(stdexec::get_stop_token) + | stdexec::then([](auto tok) { return tok.stop_requested(); }); + +The standard helpers ``stdexec::get_stop_token()``, +``stdexec::get_scheduler()``, ``stdexec::get_allocator()``, and +``stdexec::get_delegation_scheduler()`` are all defined as one-line +calls to ``read_env`` with the corresponding query. + +**When *not* to use** ``read_env`` **:** +If you only want to *use* the stop token / allocator / scheduler in your +own algorithm, you usually want the helper (e.g. ``get_stop_token()``) +rather than wiring ``read_env`` directly — the helper is the same +thing with a shorter name. + +.. _UserGuide_schedule: + +``schedule`` — start a pipeline on a scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::schedule` is how you say "begin work on *this* +execution context". You give it a *scheduler* — a lightweight handle to +a thread pool, GPU stream, event loop, etc. — and get back a sender that, +when started, value-completes (with no datums) *from the context of that +scheduler*. Anything you chain after it runs on that context. + +.. code-block:: cpp + + auto sched = stdexec::get_parallel_scheduler(); + + auto sndr = + stdexec::schedule(sched) // hop onto sched + | stdexec::then([] { return 42; }); // ... and compute on it + + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42, computed on the parallel scheduler + +The schedule-sender carries no value datum — the *point* of ``schedule`` +is the context transition, not the value. Use :cpp:member:`stdexec::then` +or :cpp:member:`stdexec::let_value` to produce the actual work. + +**Use** ``schedule`` **or** :cpp:member:`stdexec::starts_on` **or** +:cpp:member:`stdexec::continues_on` **?** + +- ``schedule(sched)`` is the *primitive*: it gives you a fresh sender on + ``sched``. Use it when you're starting a new pipeline. +- ``starts_on(sched, sndr)`` runs ``sndr`` starting on ``sched``. It is + shorthand for ``schedule(sched) | let_value([&] { return sndr; })`` (or + equivalent). +- ``continues_on(sndr, sched)`` runs ``sndr`` to completion, then + transfers execution to ``sched`` for whatever follows. Use this to + *change contexts* mid-pipeline. + +Sender adaptors +~~~~~~~~~~~~~~~ + +Adaptors take an existing sender and produce a new sender with additional +behavior. They sit in the *middle* of a pipeline. + +.. _UserGuide_then: + +``then`` — transform a value +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::then` is the asynchronous counterpart to "apply a +function to a result". You give it a predecessor sender and a callable; you +get back a new sender that, when its predecessor completes with values, +invokes the callable on those values and forwards the *return value* +downstream. If the predecessor fails or is cancelled, the callable is never +invoked and the failure or cancellation flows through unchanged. + +The simplest possible example: + +.. code-block:: cpp + + auto sndr = stdexec::just(21) + | stdexec::then([](int x) { return x * 2; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 + +``then`` can be called in two equivalent ways: + +.. code-block:: cpp + + // Direct call form: + auto s1 = stdexec::then(sndr, f); + + // Pipe (sender-adaptor-closure) form — usually preferred in chains: + auto s2 = sndr | stdexec::then(f); + +Chaining several transformations is the bread-and-butter use of ``then``: + +.. code-block:: cpp + + auto pipeline = stdexec::just(std::string{"hello"}) + | stdexec::then([](std::string s) { return s + ", world"; }) + | stdexec::then([](std::string s) { return s.size(); }); + auto [n] = stdexec::sync_wait(std::move(pipeline)).value(); + // n == 12 + +A function that returns ``void`` is allowed; the resulting sender completes +with a *value completion with no datums*. This is useful when you want to +perform a side effect mid-pipeline but have nothing to forward downstream: + +.. code-block:: cpp + + auto pipeline = stdexec::just(42) + | stdexec::then([](int x) { std::println("got {}", x); }) + | stdexec::then([] { return "done"; }); + +**What happens on error?** +If the predecessor sender completes with an error, ``then`` forwards the +error and does not invoke your callable. If your callable itself *throws*, +the exception is caught and delivered through the error channel as a +``std::exception_ptr``. To handle the error in-pipeline, follow up with +:cpp:member:`stdexec::upon_error` or :cpp:member:`stdexec::let_error`. + +**What happens on cancellation?** +If the predecessor sender completes via ``set_stopped``, ``then`` forwards +the stopped completion and does not invoke your callable. ``then`` itself +never consults the receiver's stop token. + +**When *not* to use** ``then`` **:** +If the function you want to apply *itself* returns a sender (i.e. it starts +another asynchronous operation), reach for :cpp:member:`stdexec::let_value` +instead. ``then`` would forward the returned sender as a *value* — almost +certainly not what you want. + +.. code-block:: cpp + + // Wrong: the resulting value type is a sender, not the eventual int. + auto bad = stdexec::just(7) | stdexec::then(fetch_async); + // Right: let_value chains the returned sender into the pipeline. + auto good = stdexec::just(7) | stdexec::let_value(fetch_async); + +.. _UserGuide_upon_error: + +``upon_error`` — recover from an error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::upon_error` is to the *error channel* what +:cpp:member:`stdexec::then` is to the *value channel*. You give it a +predecessor sender and a callable; if the predecessor completes with an +error, the callable is invoked with the error datum and its return value is +delivered downstream as a regular *value* completion. If the predecessor +succeeds (or is cancelled), ``upon_error`` is a no-op — your callable is +never invoked and the completion is forwarded unchanged. + +.. code-block:: cpp + + auto sndr = stdexec::just_error(std::error_code{ENOENT, std::system_category()}) + | stdexec::upon_error([](std::error_code) { return -1; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == -1 + +The error channel of the input sender is *consumed* — the resulting sender +will never complete via ``set_error`` (unless the callable itself throws, +in which case the exception is rethrown via ``set_error(exception_ptr)``). + +**What happens on success or cancellation?** +The corresponding completion is forwarded unchanged; the callable is never +invoked. + +**When *not* to use** ``upon_error`` **:** +If your recovery step *itself* needs to perform another async operation +(e.g. retry against a different server), reach for +:cpp:member:`stdexec::let_error` instead — it expects a callable that +returns a sender. + +.. _UserGuide_upon_stopped: + +``upon_stopped`` — recover from cancellation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::upon_stopped` handles the *stopped* completion. You +give it a predecessor sender and a *nullary* callable; if the predecessor +is cancelled, the callable is invoked with no arguments and its return +value is delivered downstream as a value completion. Successful values and +errors are forwarded unchanged. + +.. code-block:: cpp + + auto sndr = stdexec::just_stopped() + | stdexec::upon_stopped([] { return 42; }); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 + +Like :cpp:member:`stdexec::upon_error`, ``upon_stopped`` *consumes* its +channel — the resulting sender will not complete via ``set_stopped``. + +**When *not* to use** ``upon_stopped`` **:** +If your fallback step is itself asynchronous, reach for +:cpp:member:`stdexec::let_stopped` — it expects a callable that returns a +sender. + +.. _UserGuide_let_value: + +``let_value`` — chain another async operation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::let_value` is the way to launch *another* async +operation based on the values from a predecessor. Where ``then`` takes a +function returning a *value*, ``let_value`` takes a function returning a +*sender* — the returned sender is then run as part of the pipeline. + +.. code-block:: cpp + + auto fetch_async = [](int id) { + return stdexec::just(id * 10); // pretend this is a non-trivial async op + }; + + auto sndr = stdexec::just(7) + | stdexec::let_value(fetch_async); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 70 + +The completion signatures of the overall pipeline are the *union* of the +signatures of every sender the callable can return. So a callable that +sometimes returns a sender completing with ``int`` and sometimes with +``string`` gives a pipeline that may complete with either. + +**Use** ``then`` **or** ``let_value`` **?** +Use ``then`` when the function returns a *value*. Use ``let_value`` when +the function returns a *sender*. Passing a sender-returning function to +``then`` is almost always a bug — the resulting pipeline forwards the +sender as a value rather than running it. + +**Use** ``let_value`` **or coroutines?** +Either works; ``let_value`` is the explicit, sender-graph form, while +``co_await`` inside a ``stdexec::task`` reads more sequentially. Mix them +freely. + +.. _UserGuide_let_error: + +``let_error`` — retry asynchronously after an error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::let_error` is to :cpp:member:`stdexec::upon_error` +what :cpp:member:`stdexec::let_value` is to :cpp:member:`stdexec::then`: it +takes a callable that returns a *sender* instead of a value, so the +recovery step can itself be asynchronous (a retry, a fallback fetch, etc.). + +.. code-block:: cpp + + auto retry_async = [](std::error_code) { return stdexec::just(7); }; + + auto sndr = stdexec::just_error(std::error_code{ENOENT, std::system_category()}) + | stdexec::let_error(retry_async); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 7 + +.. _UserGuide_let_stopped: + +``let_stopped`` — fall back asynchronously after cancellation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::let_stopped` is to :cpp:member:`stdexec::upon_stopped` +what :cpp:member:`stdexec::let_value` is to :cpp:member:`stdexec::then`: it +takes a *nullary* callable that returns a *sender*, so the cancellation +fallback can itself be asynchronous. + +.. code-block:: cpp + + auto fallback_async = [] { return stdexec::just(42); }; + + auto sndr = stdexec::just_stopped() + | stdexec::let_stopped(fallback_async); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 + +Scheduling adaptors +~~~~~~~~~~~~~~~~~~~ + +Once you have a scheduler, you need to *move work onto it* — either to +begin a pipeline on a particular execution context, transfer between +contexts mid-pipeline, or take a brief detour. stdexec offers three +adaptors for this: ``starts_on``, ``continues_on``, and ``on``. They +look superficially similar; the table below disambiguates them. + +.. _UserGuide_scheduling_adaptors: + +``starts_on`` vs. ``continues_on`` vs. ``on``: which one? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 24 28 28 30 + + * - Adaptor + - Form + - Where the work runs + - Where the completion is delivered + * - ``schedule(sched)`` + - factory + - on ``sched`` + - on ``sched`` + * - ``starts_on(sched, sndr)`` + - factory-ish + - on ``sched`` + - on ``sched`` + * - ``continues_on(sndr, sched)`` + - adaptor (pipeable) + - on ``sndr``'s scheduler + - on ``sched`` + * - ``on(sched, sndr)`` + - adaptor + - on ``sched`` + - on the *start* scheduler (round-trip) + * - ``on(sndr, sched, closure)`` + - adaptor (pipeable) + - ``sndr`` in place; ``closure`` on ``sched`` + - on ``sndr``'s original completion scheduler (round-trip) + +In short: + +- Reach for **``starts_on``** when you want the whole pipeline (from + some point onward) to run on a specific scheduler and stay there. +- Reach for **``continues_on``** when you want to switch contexts at a + specific point: "produce on the I/O thread, but compute the result on + the worker pool." +- Reach for **``on``** when you want a *side trip* to another + scheduler — do some work there, then come back to where you started. + +.. _UserGuide_starts_on: + +``starts_on`` — begin work on a scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::starts_on` takes a scheduler and a sender, and +produces a sender that runs the given sender starting on the scheduler's +context. The completion is delivered on that *same* context — there is +no transfer back to the caller's scheduler. Unlike most adaptors, +``starts_on`` has no pipe form; the scheduler always comes first. + +.. code-block:: cpp + + auto sched = stdexec::get_parallel_scheduler(); + + auto sndr = + stdexec::starts_on(sched, + stdexec::just(21) + | stdexec::then([](int x) { return x * 2; })); + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42, computed on `sched` + +Equivalently — and this is the spec's defining identity: + +.. code-block:: cpp + + // starts_on(sch, sndr) is semantically equivalent to: + stdexec::schedule(sch) | stdexec::let_value([sndr]() mutable { + return std::move(sndr); + }); + +stdexec's implementation differs internally (for GPU efficiency), but +the observable semantics match. + +.. _UserGuide_continues_on: + +``continues_on`` — transfer contexts mid-pipeline +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::continues_on` takes a sender and a scheduler, runs +the sender to completion, then transfers execution to the scheduler +before forwarding the completion downstream. It's the canonical way to +*hand off* between execution contexts. + +.. code-block:: cpp + + auto io_sched = stdexec::get_parallel_scheduler(); // pretend: I/O + auto cpu_sched = stdexec::get_parallel_scheduler(); // pretend: compute + + auto sndr = + stdexec::starts_on(io_sched, stdexec::just(42)) // produce on io_sched + | stdexec::continues_on(cpu_sched) // hop to cpu_sched + | stdexec::then([](int x) { return x * 2; }); // then() runs on cpu_sched + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 84 + +``continues_on`` does *not* alter the values, errors, or stopped status +of its predecessor — it only changes the execution context they're +delivered on. If you want to also transform the value, chain a +:cpp:member:`stdexec::then` after. + +**When *not* to use** ``continues_on`` **:** +If you only want to *temporarily* run on a different scheduler and then +come back, use :cpp:member:`stdexec::on` (Form 2) — it round-trips, +``continues_on`` doesn't. + +.. _UserGuide_on: + +``on`` — take a side trip to another scheduler +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::on` is the round-trip scheduling adaptor: it runs +work on another scheduler and then *transfers execution back* to the +scheduler that started the operation. There are two forms. + +**Form 1 — ``on(sched, sndr)``** runs the entirety of ``sndr`` on +``sched`` and then returns to the start scheduler: + +.. code-block:: cpp + + auto sched = stdexec::get_parallel_scheduler(); + auto sndr = stdexec::on(sched, stdexec::just(21) + | stdexec::then([](int x){ return x*2; })); + +This differs from ``starts_on(sched, ...)`` in *exactly one* way: after +``sndr`` completes, ``on`` transfers back to wherever the operation +originated; ``starts_on`` stays put. Use ``on`` when downstream code +needs to run on the caller's scheduler again. + +**Form 2 — ``on(sndr, sched, closure)``** (and the pipe form +``sndr | on(sched, closure)``) is the "side trip" pattern. The +predecessor runs on its own scheduler; we hop to ``sched`` for the +closure; we hop back when the closure completes: + +.. code-block:: cpp + + auto gpu = stdexec::get_parallel_scheduler(); // pretend: GPU + + auto sndr = + stdexec::just(21) + | stdexec::on(gpu, stdexec::then([](int x) { return x * 2; })); + // ^^^^^^^^^^^^^^^ the then() inside runs on `gpu`, but + // sync_wait() below sees the result on its + // own context — we round-tripped. + auto [v] = stdexec::sync_wait(std::move(sndr)).value(); + // v == 42 + +Use Form 2 when a small, well-defined chunk of your pipeline needs a +different scheduler (a compute-bound transform, a GPU kernel, a blocking +syscall hidden in a thread pool) and the rest should stay where it is. + +**Picking a form.** +If you're starting a fresh pipeline and want to *stay* on a scheduler, +use :cpp:member:`stdexec::starts_on`. If you want to hand off +permanently to a new scheduler, use :cpp:member:`stdexec::continues_on`. +If you want a side trip and then back, use ``on``. + +Composition adaptors +~~~~~~~~~~~~~~~~~~~~ + +So far every adaptor we've seen takes one sender and produces another. +Composition adaptors take *many* senders and combine them into one — +they're how you express parallel and fan-out patterns. + +.. _UserGuide_when_all: + +``when_all`` — run senders concurrently and gather their values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::when_all` is the parallel-composition primitive +of the sender model. You give it one or more senders; you get back a +single sender that, when started, runs all of them concurrently. When +every input has completed, ``when_all``'s sender value-completes with a +tuple that is the *concatenation* of all the inputs' value datums. + +.. code-block:: cpp + + auto sndr = stdexec::when_all( + stdexec::just(1), + stdexec::just(2.5), + stdexec::just(std::string{"x"})); + auto [i, d, s] = stdexec::sync_wait(std::move(sndr)).value(); + // i == 1, d == 2.5, s == "x" + +Two key things to internalize: + +1. **Lazy, not eager.** Like every other adaptor, ``when_all`` does + *nothing* until its result sender is connected and started. The + inputs aren't running yet just because you named them in a call to + ``when_all`` — they're stored, and they all start the moment the + outer pipeline starts. + +2. **"Concurrently" means "not sequenced".** The inputs are started in + a fold over the pack — they're not awaited in order. Whether they + actually execute in parallel depends on the schedulers they're + attached to. To get true parallelism, chain each branch through its + own :cpp:member:`stdexec::starts_on`: + + .. code-block:: cpp + + auto cpu = stdexec::get_parallel_scheduler(); + + auto sndr = stdexec::when_all( + stdexec::starts_on(cpu, sndr_a), + stdexec::starts_on(cpu, sndr_b), + stdexec::starts_on(cpu, sndr_c)); + + Without that, all the branches just run synchronously inside the + caller's :cpp:member:`stdexec::start` (still useful for type-level + composition, but not actually parallel). + +**Fail-fast semantics.** +If any one input fails (``set_error``) or is stopped (``set_stopped``), +``when_all`` *requests stop* on all the others via an internal stop +source and completes with that error/stopped completion. The first one +observed wins; subsequent failures are discarded. This makes +``when_all`` naturally short-circuiting on errors — siblings get a +chance to wind down promptly instead of running to completion. + +**Single value-completion per input.** +``when_all`` requires that each input sender have *exactly one* +``set_value_t`` completion shape — otherwise the output's value type +would explode into all possible concatenations. If you have inputs with +multiple shapes, use :cpp:member:`stdexec::when_all_with_variant`, +which wraps each in a ``std::variant`` first. + +**``when_all`` vs.** :cpp:member:`stdexec::spawn_future` **:** +Both can express "run N things concurrently and collect their results," +but they differ on *when* the work starts. ``when_all`` is lazy: the +work starts when the composed sender is started. ``spawn_future`` is +eager: the work starts the moment you call it, and you observe results +later through the returned sender. Use ``when_all`` for composition +inside a pipeline; use ``spawn_future`` when you want to overlap async +work with synchronous code or to start work before you know how many +results you'll need to collect. + +.. _UserGuide_when_all_with_variant: + +``when_all_with_variant`` — like ``when_all`` for multi-shape inputs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::when_all_with_variant` is the multi-completion +sibling of ``when_all``. It wraps each input in +:cpp:member:`stdexec::into_variant`, so an input that may +value-complete with either ``int`` or ``std::string`` is collapsed into +a single ``std::variant, std::tuple>`` +before being passed to the ordinary ``when_all`` machinery. + +.. code-block:: cpp + + // sndr_a value-completes with either set_value_t(int) or set_value_t(std::string); + // sndr_b value-completes with set_value_t(float). + auto sndr = stdexec::when_all_with_variant(sndr_a, sndr_b); + auto [va, vb] = stdexec::sync_wait(std::move(sndr)).value(); + // va: std::variant, std::tuple> + // vb: std::variant> + +If every input has a single value-completion shape, prefer plain +``when_all`` — it produces friendlier ``std::tuple`` values directly. + +.. _UserGuide_into_variant: + +``into_variant`` — collapse multi-completion senders +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::into_variant` reshapes a sender that can +value-complete in more than one way into a sender that always +value-completes with a single ``std::variant``-of-tuples datum. It is +the primitive behind :cpp:member:`stdexec::when_all_with_variant` and +:cpp:member:`stdexec::sync_wait_with_variant`, but you can use it +directly whenever a downstream algorithm wants the single-completion +form. + +.. code-block:: cpp + + // sndr value-completes with either set_value_t(int) or set_value_t(std::string). + auto single = stdexec::into_variant(sndr); + // single value-completes with: + // set_value_t(std::variant, std::tuple>) + auto [v] = stdexec::sync_wait(std::move(single)).value(); + std::visit([](auto&& tup) { /* ... */ }, v); + +The pipe form ``sndr | into_variant()`` is equivalent. Note the *empty* +parentheses — there are no other arguments to pass; the closure exists +purely for the pipe syntax. + +Parallel-loop adaptors +~~~~~~~~~~~~~~~~~~~~~~ + +.. _UserGuide_bulk: + +``bulk`` — apply a function over an index space +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::bulk` is the parallel-loop primitive of the +sender model. You give it a sender, an *execution policy*, an integral +*shape*, and a callable; you get back a sender that, when started, +invokes the callable once for each integer in ``[0, shape)``. + +.. code-block:: cpp + + std::vector buf(1024, 0); + + auto pipeline = stdexec::just() + | stdexec::bulk(stdexec::par, buf.size(), + [&](std::size_t i) { buf[i] = compute(i); }); + stdexec::sync_wait(std::move(pipeline)).value(); + +The callable receives the index as its first argument; any values from +the predecessor's value completion are passed as additional arguments +(and shared across all iterations — *the same values, not a per-index +view*). + +The execution policy works like the policies in ````: + +- ``stdexec::seq`` — sequential, no parallelism. +- ``stdexec::par`` — parallelism permitted. +- ``stdexec::par_unseq`` — parallelism *and* vectorization permitted. + +Whether iterations actually run in parallel depends on the scheduler. +On an :ref:`inline scheduler ` +(the implicit one used by ``just`` plus ``sync_wait``) every iteration +runs synchronously on the calling thread regardless of the policy. On +a thread-pool or GPU scheduler — typically used in conjunction with +:cpp:member:`stdexec::starts_on` — the policy is honored. + +**Two variants.** + +:cpp:member:`stdexec::bulk_chunked` invokes the callable with *ranges* +``(begin, end, vs...)`` instead of single indices. Use it when the +per-iteration body benefits from per-chunk amortization +(thread-local accumulators, vectorization setup, batched allocations). +``bulk`` is internally implemented in terms of ``bulk_chunked`` and +delegates the chunk-size decisions to the runtime. + +:cpp:member:`stdexec::bulk_unchunked` is like ``bulk`` but explicitly +*forbids* chunking — each index is guaranteed its own invocation. Use +when per-iteration state cannot be batched. + +**``bulk`` as the GPU hook.** +A custom scheduler can take over ``bulk`` via the +:ref:`domain customization ` +mechanism, lowering it to a parallel-kernel launch on its own +execution context. nvexec does exactly this for CUDA. + +Stopped-channel translators +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These adaptors *re-route* a stopped completion onto a different +channel. They don't change the underlying behavior; they translate. + +.. _UserGuide_stopped_as_error: + +``stopped_as_error`` — turn cancellation into an error +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::stopped_as_error` converts a ``set_stopped`` +completion into a ``set_error`` completion carrying a caller-supplied +error. Use it when downstream code can't (or shouldn't) distinguish +cancellation from other failure modes. + +.. code-block:: cpp + + auto sndr = stdexec::just_stopped() + | stdexec::stopped_as_error(std::runtime_error{"cancelled"}); + + try { + stdexec::sync_wait(std::move(sndr)); + } catch (std::runtime_error const& e) { + assert(std::string{e.what()} == "cancelled"); + } + +It's a thin wrapper over :cpp:member:`stdexec::let_stopped` + +:cpp:member:`stdexec::just_error` — reach for it whenever you would +have written that pattern by hand. + +.. _UserGuide_stopped_as_optional: + +``stopped_as_optional`` — turn cancellation into ``std::nullopt`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::stopped_as_optional` is the *value-channel* +analogue. It converts a ``set_stopped`` completion into a +*value* completion of ``std::optional{std::nullopt}``, wrapping the +predecessor's value (when it completes successfully) in a +``std::optional``. + +.. code-block:: cpp + + auto sndr = stdexec::just(42) | stdexec::stopped_as_optional(); + auto [opt] = stdexec::sync_wait(std::move(sndr)).value(); + // opt == std::optional{42} + +The predecessor must have *exactly one* value-completion signature with +exactly one argument — otherwise the resulting sender wouldn't have a +unique ``std::optional`` shape to use. The static assertion will +say so. + +**Use** ``stopped_as_optional`` **or** ``sync_wait`` **?** + +:cpp:member:`stdexec::sync_wait` already returns +``std::optional>`` and gives you ``nullopt`` on stopped. +Use ``stopped_as_optional`` when you want that optional-shape *inside +the pipeline* rather than at the consumer — for example, to feed it +into a :cpp:member:`stdexec::then` that branches on the optional. + +Environment adaptors +~~~~~~~~~~~~~~~~~~~~ + +.. _UserGuide_write_env: + +``write_env`` — inject values into a sub-pipeline's environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::write_env` is the inverse of +:cpp:member:`stdexec::read_env`. ``read_env`` *reads* a value from the +receiver's environment and exposes it on the value channel; +``write_env`` *injects* values into the environment a sub-pipeline +sees. The supplied environment is overlaid on the receiver's +environment, so child senders see the merged view. + +.. code-block:: cpp + + auto inner = stdexec::read_env(stdexec::get_stop_token) + | stdexec::then([](auto tok) { return tok.stop_requested(); }); + + stdexec::stop_source src; + auto pipeline = + inner + | stdexec::write_env(stdexec::prop{stdexec::get_stop_token, src.get_token()}); + + auto [requested] = stdexec::sync_wait(std::move(pipeline)).value(); + // The inner pipeline sees `src`'s token, not the outer pipeline's. + +Common uses: + +- Injecting a different stop token so a sub-pipeline can be cancelled + independently of the surrounding work. +- Supplying an allocator to a sub-pipeline that allocates internally. +- Pinning a domain when a sender doesn't have a scheduler in its + chain. + +The supplied environment shadows the receiver's environment for any +query it can answer; queries it can't answer fall through. + +Sender consumers +~~~~~~~~~~~~~~~~ + +Consumers are how a pipeline actually *runs*. They take a sender, +connect it to a built-in receiver, and start the resulting operation. +Until a consumer is called, a sender does nothing — it is just a +description of work. + +.. _UserGuide_sender_consumers: + +Picking a consumer +^^^^^^^^^^^^^^^^^^ + +Five consumers cover the common cases. The first question to ask is: +*does my caller need to wait for the result?* + +.. list-table:: + :header-rows: 1 + :widths: 22 26 30 22 + + * - Consumer + - Returns + - Use when + - Eager or lazy? + * - :cpp:member:`stdexec::sync_wait` + - ``std::optional>`` + - Top-level synchronous wait; single value-completion shape. + - lazy + * - :cpp:member:`stdexec::sync_wait_with_variant` + - ``std::optional...>>`` + - Same, but the sender has multiple value-completion shapes. + - lazy + * - :cpp:member:`exec::start_detached` + - ``void`` + - Top-level fire-and-forget; no owning scope. **stdexec extension.** + - eager + * - :cpp:member:`stdexec::spawn` + - ``void`` + - Fire-and-forget into an async scope that will be joined later. + - eager + * - :cpp:member:`stdexec::spawn_future` + - sender + - Spawn into a scope *and* observe the result without blocking. + - eager + +The other axis is *who owns the lifetime of the operation state*: + +- For ``sync_wait`` / ``sync_wait_with_variant`` the *caller's stack + frame* owns it (the operation runs synchronously to completion). +- For ``start_detached`` the operation owns itself — it heap-allocates + and deallocates on completion. +- For ``spawn`` and ``spawn_future`` the *scope* owns it: the operation + is associated with a scope token that must outlive the work and is + eventually joined. + +.. _UserGuide_sync_wait: + +``sync_wait`` — block until a result is ready +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::sync_wait` is the bridge from senders back into +synchronous code. It connects the sender, drives an internal +``run_loop`` on the calling thread until completion, and returns the +result wrapped in a ``std::optional>``. + +.. code-block:: cpp + + auto [v] = stdexec::sync_wait(stdexec::just(42)).value(); + // v == 42 + +The return shape is uniform: an engaged optional on ``set_value``, a +disengaged optional on ``set_stopped``, and a thrown exception on +``set_error`` (rethrown directly for ``std::exception_ptr``, wrapped in +``std::system_error`` for ``std::error_code``, thrown as-is otherwise). + +.. code-block:: cpp + + if (auto result = stdexec::sync_wait(std::move(sndr))) { + auto [v] = *result; // succeeded + } else { + // cancelled (set_stopped) + } + +**Single value-completion only.** +``sync_wait`` requires a sender with exactly one ``set_value_t`` +completion signature. If the sender can succeed in more than one way, +the static assertion will steer you to :cpp:member:`stdexec::sync_wait_with_variant`. + +**Don't use** ``sync_wait`` **on an executor thread.** It blocks. It is +for top-level code (``main``, tests, leaf utilities), not for the +middle of a pipeline. If you need to "wait" mid-pipeline, you almost +certainly want :cpp:member:`stdexec::let_value` or a coroutine +``co_await`` instead. + +.. _UserGuide_sync_wait_with_variant: + +``sync_wait_with_variant`` — block until a multi-shape result is ready +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::sync_wait_with_variant` is for the case where a +sender can succeed in more than one way. The result is wrapped in a +``std::optional...>>``: + +.. code-block:: cpp + + // sndr can complete with either set_value_t(int) or set_value_t(std::string) + if (auto opt = stdexec::sync_wait_with_variant(std::move(sndr))) { + std::visit([](auto&& tup) { + // tup is std::tuple or std::tuple + }, *opt); + } + +If your sender has only one value-completion shape, use +:cpp:member:`stdexec::sync_wait` — it returns a friendlier +``std::tuple`` directly. + +.. _UserGuide_start_detached: + +``start_detached`` *(extension)* — fire and forget +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`exec::start_detached` eagerly starts a sender and discards +its result. The operation state is heap-allocated and self-destructs on +completion. The sender **must not** complete with ``set_error`` — there +is no caller to deliver the error to. The static assertion enforces +this; if your sender can fail, handle the error inline with +:cpp:member:`stdexec::upon_error` / :cpp:member:`stdexec::let_error` +first. + +.. code-block:: cpp + + exec::start_detached( + stdexec::just(42) + | stdexec::then([](int x) { std::println("background: {}", x); })); + +This is an **stdexec extension** — it isn't in the C++26 working draft. +The standardized scope-tracked equivalent is +:cpp:member:`stdexec::spawn`; reach for that when you have an async +scope that should own the work's lifetime. + +.. _UserGuide_spawn: + +``spawn`` — fire and forget into a scope +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::spawn` is the standardized way to launch +fire-and-forget work whose lifetime is owned by an *async scope*. You +pass a sender and a *scope token* (a handle you get from an async +scope); ``spawn`` allocates and starts the operation, and the scope +tracks it for later joining. + +.. code-block:: cpp + + exec::async_scope scope; + + stdexec::spawn( + stdexec::just(42) + | stdexec::then([](int x) { std::println("background: {}", x); }), + scope.get_token()); + + // ... later, before scope is destroyed ... + stdexec::sync_wait(scope.join()); + +As with ``start_detached``, the sender must not be able to complete +with ``set_error`` — ``spawn`` cannot deliver an error to a +non-existent caller. + +**``spawn`` vs.** :cpp:member:`exec::start_detached` **:** +Prefer ``spawn`` whenever there's a natural owning scope (a request, a +session, a worker, the program as a whole). Reserve ``start_detached`` +for one-shot top-level work where adding a scope would be ceremony. + +.. _UserGuide_spawn_future: + +``spawn_future`` — fire and observe +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:cpp:member:`stdexec::spawn_future` is ``spawn`` plus an observation +channel. It eagerly starts the sender into the scope *and* returns a +sender that, when later connected and started, delivers the spawned +operation's result. + +.. code-block:: cpp + + exec::async_scope scope; + + auto future = + stdexec::spawn_future(stdexec::just(42) + | stdexec::then([](int x){ return x * 2; }), + scope.get_token()); + + // The work is already running. Do something else here ... + + auto [v] = stdexec::sync_wait(std::move(future)).value(); + // v == 84 + + stdexec::sync_wait(scope.join()); + +The key thing to internalize is that the work is **eager**: it starts +at the moment ``spawn_future`` is called, not when you connect the +returned sender. The returned sender is a *one-shot observer* of work +that is already running. This is what makes ``spawn_future`` good for +fan-out: spawn N pieces of concurrent work, collect their results +individually. + +**``spawn_future`` vs.** :cpp:member:`stdexec::when_all` **:** +``when_all`` is *lazy* — it composes senders without starting them, +and the resulting sender only runs the children when *it* is started. +``spawn_future`` is the right choice when work needs to start +*immediately* (perhaps to overlap with synchronous code) and you'll +collect results later. + 🔄 Coroutine Integration ------------------------ -Senders can be ``co_await``-ed in coroutines if they model ``awaitable_sender``. Any sender -that can complete successfully in exactly one way is an awaitable sender. +Senders can be ``co_await``-ed inside a coroutine whose promise type +participates in stdexec's awaitable-sender protocol (e.g. +``stdexec::task``). Any sender with exactly one successful completion +shape is awaitable in such a coroutine. .. code-block:: cpp diff --git a/include/exec/start_detached.hpp b/include/exec/start_detached.hpp index a3938c22b..1aabab520 100644 --- a/include/exec/start_detached.hpp +++ b/include/exec/start_detached.hpp @@ -145,6 +145,61 @@ namespace experimental::execution && STDEXEC::__same_as>; } // namespace __start_detached + //! @brief A sender consumer that eagerly starts a sender and forgets it. + //! + //! @c start_detached connects its argument sender to a built-in receiver + //! and starts the resulting operation immediately, allocating the + //! operation state on the heap (using an allocator from the optional + //! environment) so it can outlive the call. The completion of the sender + //! deallocates the operation state and discards the result; nothing is + //! returned to the caller. + //! + //! Use @c start_detached for top-level fire-and-forget work that has no + //! caller waiting on its result and no enclosing async scope — for + //! example, kicking off a logging or telemetry pipeline from @c main(), + //! or a one-shot background task at program startup. For fire-and-forget + //! work that should be *tracked* by a scope (so the scope can be joined + //! at shutdown), prefer @c stdexec::spawn. For top-level *waiting*, use + //! @c stdexec::sync_wait. + //! + //! @note @c start_detached is an @b stdexec @b extension. It is not part + //! of the C++26 working draft. The standardized way to spawn + //! fire-and-forget work is @c stdexec::spawn (see [exec.spawn]), + //! which requires an async scope to take ownership of the + //! operation. + //! + //! @code{.cpp} + //! exec::start_detached(stdexec::just(42) | stdexec::then([](int x) { + //! std::println("background work produced {}", x); + //! })); + //! @endcode + //! + //! **Completion behavior.** + //! + //! The sender must complete via @c set_value or @c set_stopped — both + //! are accepted and the result is discarded. The sender *must not* + //! complete via @c set_error: there is no caller to deliver the error + //! to, and an error completion is therefore considered a contract + //! violation. The implementation enforces this with a static assertion + //! when possible; if the sender's completion signatures include + //! @c set_error_t the program is ill-formed. + //! + //! **Allocator support.** + //! + //! The two-argument overload accepts an environment from which an + //! allocator can be queried (via @c stdexec::get_allocator). That + //! allocator is used to allocate the operation state, so callers can + //! avoid the default global @c new for hot paths. + //! + //! **Cancellation.** + //! + //! @c start_detached does not arrange for cancellation of the spawned + //! work. If the operation observes a stop token via the environment, + //! it can self-cancel; otherwise the work runs to natural completion. + //! + //! @see stdexec::sync_wait — top-level synchronous wait that returns the result + //! @see stdexec::spawn — fire-and-forget into a scope (standardized in C++26) + //! @see stdexec::spawn_future — spawn into a scope and observe via a sender struct start_detached_t { template @@ -155,6 +210,14 @@ namespace experimental::execution _Sender, STDEXEC::__as_root_env_t<_Env>>; + //! @brief Eagerly start @c __sndr; allocate its operation state on the + //! heap with the default allocator. + //! + //! @tparam _Sender A sender type with no @c set_error_t completions. + //! @param __sndr The sender to launch. Forwarded into the heap-allocated + //! operation state. + //! + //! @pre @c __sndr must not be able to complete with @c set_error. template _Sender> requires STDEXEC::__callable, @@ -166,6 +229,19 @@ namespace experimental::execution STDEXEC::apply_sender(__domain_t{}, *this, static_cast<_Sender&&>(__sndr)); } + //! @brief Eagerly start @c __sndr; use the allocator from @c __env to + //! allocate the operation state. + //! + //! @tparam _Env An environment type. Queried with + //! @c stdexec::get_allocator for an allocator; + //! falls back to @c std::allocator if absent. + //! @tparam _Sender A sender type with no @c set_error_t completions. + //! + //! @param __sndr The sender to launch. + //! @param __env The environment used both for the allocator query + //! and for the receiver's environment. + //! + //! @pre @c __sndr must not be able to complete with @c set_error. template > _Sender> requires STDEXEC::__callable>, @@ -226,6 +302,12 @@ namespace experimental::execution } }; + //! @brief The customization point object for the @c start_detached sender consumer. + //! + //! @c start_detached is an instance of @ref start_detached_t. See + //! @ref start_detached_t for the full description and a usage example. + //! + //! @hideinitializer inline constexpr start_detached_t start_detached{}; } // namespace experimental::execution diff --git a/include/stdexec/__detail/__bulk.hpp b/include/stdexec/__detail/__bulk.hpp index f3ccbc156..6bc97f83c 100644 --- a/include/stdexec/__detail/__bulk.hpp +++ b/include/stdexec/__detail/__bulk.hpp @@ -374,6 +374,76 @@ namespace STDEXEC }; } // namespace __bulk + //! @brief A pipeable sender adaptor that applies a function to each index + //! in @c [0, shape) under a given execution policy. + //! + //! @c bulk is the parallel-loop primitive of the sender model. You give + //! it a sender, an execution policy (e.g. @c stdexec::par), an integral + //! shape, and a callable; you get back a sender that, when started, + //! invokes f(i, vs...) for every @c i in @c [0, shape) — where + //! @c vs... are the predecessor's value-completion datums. The + //! execution policy controls whether the invocations may run in + //! parallel. + //! + //! See [exec.bulk] in the C++26 working draft. + //! + //! The signature of the operator overloads (inherited from a detail + //! base) is: + //! + //! @code{.cpp} + //! template + //! auto operator()(Sender&& sndr, Policy&& pol, + //! Shape shape, Fun fun) const -> sender auto; // direct + //! + //! template + //! auto operator()(Policy&& pol, Shape shape, Fun fun) const; // closure + //! @endcode + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::bulk(sndr, stdexec::par, 1024, fn); + //! auto s2 = sndr | stdexec::bulk(stdexec::par, 1024, fn); + //! @endcode + //! + //! **Completion signatures.** + //! + //! @c bulk forwards the predecessor's completion signatures, optionally + //! adding @c set_error_t(std::exception_ptr) if invoking @c fn may throw + //! (or if internal allocation may throw): + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged from sndr + //! set_error_t(Es)... // forwarded unchanged from sndr + //! set_error_t(std::exception_ptr) // added if fn may throw + //! set_stopped_t() // forwarded unchanged (if present) + //! @endcode + //! + //! @c fn is invoked with the *value-completion datums* of @c sndr + //! preserved across all invocations — every iteration sees the same + //! @c vs... + //! + //! **Execution policy.** + //! + //! The policy argument follows the @c conventions: + //! @c stdexec::seq for sequenced execution, @c stdexec::par for + //! permitted-parallel, @c stdexec::par_unseq for permitted parallel and + //! vectorized. A custom scheduler's domain may interpret these + //! differently — e.g. a GPU domain may lower @c par to a CUDA kernel + //! launch. + //! + //! **Implementation note: lowering to** @c bulk_chunked **.** + //! + //! Internally, @c bulk is implemented in terms of + //! @ref bulk_chunked_t — its @c transform_sender member rewrites + //! bulk(sndr, pol, n, f) into a @c bulk_chunked over the same + //! shape with @c f wrapped in a per-chunk loop. If a domain customizes + //! @c bulk_chunked, @c bulk picks up that customization automatically. + //! + //! @see stdexec::bulk_chunked — explicit-chunk variant + //! @see stdexec::bulk_unchunked — strict per-index variant (no chunking allowed) + //! @see stdexec::when_all — concurrent composition without an index space struct bulk_t : __bulk::__generic_bulk_t { template @@ -389,14 +459,109 @@ namespace STDEXEC } }; + //! @brief A pipeable sender adaptor that invokes a function with chunked + //! sub-ranges of an integer index space. + //! + //! Where @ref bulk_t passes a *single index* to its callable, + //! @c bulk_chunked passes a *half-open range* @c [begin, end) covering + //! some subset of @c [0, shape). The implementation may split @c [0, + //! shape) into any number of chunks (including one chunk equal to the + //! whole range, or @c shape chunks of one element each) — the only + //! guarantee is that every index in @c [0, shape) is covered by exactly + //! one chunk. + //! + //! See [exec.bulk] in the C++26 working draft. + //! + //! The signature of the operator overloads (inherited from a detail + //! base) is: + //! + //! @code{.cpp} + //! template + //! auto operator()(Sender&& sndr, Policy&& pol, + //! Shape shape, Fun fun) const -> sender auto; // direct + //! + //! template + //! auto operator()(Policy&& pol, Shape shape, Fun fun) const; // closure + //! @endcode + //! + //! The callable is invoked as fun(begin, end, vs...), where + //! @c vs... are the predecessor's value-completion datums (shared across + //! all chunks). + //! + //! **When to use** @c bulk_chunked **vs.** @c bulk **:** + //! + //! Use @c bulk when the per-iteration body is small and the loop is the + //! payload — @c bulk's lowering to @c bulk_chunked will let the runtime + //! pick chunk sizes for you. Use @c bulk_chunked directly when the body + //! benefits from per-chunk amortization (allocations, accumulators, + //! vectorization setup) that you want to do once per chunk rather than + //! once per index. + //! + //! @see stdexec::bulk — index-at-a-time variant (lowers to this) + //! @see stdexec::bulk_unchunked — strict per-index variant (no chunking) struct bulk_chunked_t : __bulk::__generic_bulk_t {}; + //! @brief A pipeable sender adaptor that invokes a function once per index + //! in @c [0, shape), *without* permission to chunk. + //! + //! @c bulk_unchunked has the same per-index invocation pattern as + //! @ref bulk_t — fun(i, vs...) for every @c i — but explicitly + //! forbids the implementation from combining multiple indices into a + //! single call. Spec-recommended (but not required) practice is for + //! each iteration to run on a *distinct* execution agent. + //! + //! Use this only when the body of the loop has per-iteration state or + //! synchronization that *cannot* be batched — e.g., per-thread-local + //! accumulators, per-index hardware resources, observable side effects + //! that must be one-per-index. For ordinary parallel loops, prefer + //! @ref bulk_t (which lowers to @c bulk_chunked and lets the runtime + //! make chunk-size decisions). + //! + //! See [exec.bulk] in the C++26 working draft. + //! + //! The signature of the operator overloads (inherited from a detail + //! base) is: + //! + //! @code{.cpp} + //! template + //! auto operator()(Sender&& sndr, Policy&& pol, + //! Shape shape, Fun fun) const -> sender auto; // direct + //! + //! template + //! auto operator()(Policy&& pol, Shape shape, Fun fun) const; // closure + //! @endcode + //! + //! @see stdexec::bulk — chunking-permitted index-at-a-time variant + //! @see stdexec::bulk_chunked — explicit-chunk variant struct bulk_unchunked_t : __bulk::__generic_bulk_t {}; - inline constexpr bulk_t bulk{}; - inline constexpr bulk_chunked_t bulk_chunked{}; + //! @brief The customization point object for the @c bulk sender adaptor. + //! + //! @c bulk is an instance of @ref bulk_t. See @ref bulk_t for the full + //! description, the lowering to @c bulk_chunked, and a usage example. + //! + //! @hideinitializer + inline constexpr bulk_t bulk{}; + + //! @brief The customization point object for the @c bulk_chunked sender adaptor. + //! + //! @c bulk_chunked is an instance of @ref bulk_chunked_t. See + //! @ref bulk_chunked_t for the full description. + //! + //! @hideinitializer + inline constexpr bulk_chunked_t bulk_chunked{}; + + //! @brief The customization point object for the @c bulk_unchunked sender adaptor. + //! + //! @c bulk_unchunked is an instance of @ref bulk_unchunked_t. See + //! @ref bulk_unchunked_t for the full description and when to reach for + //! it. + //! + //! @hideinitializer inline constexpr bulk_unchunked_t bulk_unchunked{}; template <> diff --git a/include/stdexec/__detail/__connect.hpp b/include/stdexec/__detail/__connect.hpp index 264859130..8fe3a2eb4 100644 --- a/include/stdexec/__detail/__connect.hpp +++ b/include/stdexec/__detail/__connect.hpp @@ -224,8 +224,98 @@ namespace STDEXEC ///////////////////////////////////////////////////////////////////////////// // connect_t + + //! @brief Customization point object that *connects* a sender to a + //! receiver, producing an operation state. + //! + //! @c connect is the central seam of the sender model. Calling + //! @c stdexec::connect(sndr, rcvr) does *not* run the sender; it + //! produces an *operation state* — an opaque, immovable, startable + //! object that, when subsequently passed to @c stdexec::start, will + //! eventually deliver exactly one completion signal (@c set_value, + //! @c set_error, or @c set_stopped) to the receiver. + //! + //! Every sender adaptor and consumer ultimately reaches for @c connect. + //! Most user code does not call it directly — @c sync_wait, @c spawn, + //! and the various adaptors do — but it is the operation a sender + //! author *must* support, either by exposing a `.connect(receiver)` + //! member, by being a coroutine awaitable (so the fallback awaitable + //! adapter applies), or — historically — via @c tag_invoke (now + //! deprecated). + //! + //! See [exec.connect] in the C++26 working draft for the normative + //! specification. + //! + //! **Lookup.** + //! + //! Before dispatching, @c connect transforms the sender via the active + //! domain's @c transform_sender (passing in @c get_env(rcvr) as the + //! environment). This is how domain-based customization — e.g. the + //! GPU scheduler taking over a `then` chain — is implemented. + //! + //! @c connect then dispatches by trying, in order: + //! + //! 1. A static member: S::__static_connect(sndr, rcvr) + //! (an stdexec-internal extension point). + //! 2. A non-static member: sndr.connect(rcvr). This is the + //! standard way sender authors customize @c connect in C++26. + //! 3. The awaitable fallback: if @c sndr is awaitable in stdexec's + //! receiver-promise type, an adapter operation state is synthesized. + //! This is what makes coroutines work as senders. + //! 4. tag_invoke(connect, sndr, rcvr) — deprecated, retained + //! for backwards compatibility. + //! + //! **Customization.** + //! + //! The standard pattern for sender authors is the `.connect()` member: + //! + //! @code{.cpp} + //! struct my_sender { + //! using sender_concept = stdexec::sender_tag; + //! using completion_signatures = stdexec::completion_signatures< + //! stdexec::set_value_t(int)>; + //! + //! template R> + //! auto connect(R rcvr) && -> my_opstate { + //! return my_opstate{std::move(rcvr), ...}; + //! } + //! }; + //! @endcode + //! + //! The returned object must satisfy @c stdexec::operation_state. In + //! particular, it must be immovable once constructed (typically by + //! deleting the move and copy constructors). + //! + //! **Concept checks.** + //! + //! @c connect(s, r) is only well-formed when both + //! `sender_in>` and `receiver_of>>` + //! hold. The diagnostics for failures here are intentionally focused — + //! stdexec emits messages that name the specific completion signal or + //! environment query the receiver doesn't accept. + //! + //! @see stdexec::start — what you call on the returned operation state + //! @see stdexec::operation_state — the concept the result satisfies + //! @see stdexec::sender_to — the concept this CPO drives + //! @see stdexec::set_value — one of the completions the operation eventually delivers + //! @see stdexec::transform_sender — the domain-customization step run before dispatch struct connect_t { + //! @brief Connect @c __sndr to @c __rcvr, returning an operation state. + //! + //! @tparam _Sender A type modeling @c stdexec::sender_in for + //! @c env_of_t<_Receiver>. + //! @tparam _Receiver A type modeling @c stdexec::receiver_of for + //! @c __sndr's completion signatures. + //! + //! @param __sndr The sender describing the asynchronous work. + //! Perfect-forwarded into the operation state. + //! @param __rcvr The receiver that will eventually receive a + //! completion. Perfect-forwarded into the operation + //! state. + //! + //! @returns An object satisfying @c stdexec::operation_state. Pass it + //! to @c stdexec::start to begin the work. template > @@ -266,6 +356,13 @@ namespace STDEXEC } }; + //! @brief The customization point object for connecting a sender to a + //! receiver. + //! + //! @c connect is an instance of @ref connect_t. See @ref connect_t for + //! the full description, the lookup order, and customization examples. + //! + //! @hideinitializer inline constexpr connect_t connect{}; template diff --git a/include/stdexec/__detail/__continues_on.hpp b/include/stdexec/__detail/__continues_on.hpp index e64fa2359..238891faa 100644 --- a/include/stdexec/__detail/__continues_on.hpp +++ b/include/stdexec/__detail/__continues_on.hpp @@ -383,8 +383,103 @@ namespace STDEXEC }; } // namespace __trnsfr + //! @brief A pipeable sender adaptor that transfers a predecessor sender's + //! completion to a different scheduler's execution resource. + //! + //! @c continues_on lets a sender pipeline *change execution context* in the + //! middle. Given a predecessor sender @c sndr and a scheduler @c sched, + //! @c continues_on produces a sender that, when connected and started, + //! runs @c sndr to completion on whatever context @c sndr ran on, then + //! transfers execution to @c sched's resource, and only *then* forwards + //! @c sndr's completion (value, error, or stopped) to the connected + //! receiver. Anything chained after @c continues_on therefore runs on + //! @c sched. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::continues_on(sndr, sched); // direct invocation + //! auto s2 = sndr | stdexec::continues_on(sched); // pipe syntax + //! @endcode + //! + //! The two forms are expression-equivalent. See [exec.continues.on] in + //! the C++26 working draft for the normative specification. + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded — but delivered on `sched`'s resource + //! set_error_t(Es)... // forwarded — but delivered on `sched`'s resource + //! set_stopped_t() // forwarded — but delivered on `sched`'s resource + //! @endcode + //! + //! the sender produced by continues_on(sndr, sched) has the same + //! completion signatures as @c sndr, except that a + //! @c set_error_t(std::exception_ptr) completion may be added if any of + //! @c sndr's completion datums are not @c nothrow decay-copyable (the + //! datums must be stored across the scheduling hop). + //! + //! @c continues_on does *not* alter @c sndr's values or errors; it only + //! changes the execution context on which the completion is delivered. + //! + //! **Exception behavior.** + //! + //! If decay-copying a completion datum across the scheduling hop throws, + //! the exception is delivered through @c set_error_t(std::exception_ptr). + //! If scheduling onto @c sched fails, an error completion is delivered on + //! an unspecified execution agent. + //! + //! **Cancellation.** + //! + //! @c continues_on respects the receiver's stop token while waiting to + //! be scheduled onto @c sched: if cancellation is requested after @c sndr + //! completes but before the hop finishes, the resulting sender typically + //! completes via @c set_stopped. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto io_sched = get_parallel_scheduler(); // pretend: I/O + //! auto cpu_sched = get_parallel_scheduler(); // pretend: compute + //! + //! auto sndr = + //! starts_on(io_sched, just(42)) // produce on io_sched + //! | continues_on(cpu_sched) // hop to cpu_sched + //! | then([](int x) { return x * 2; }); // then() runs on cpu_sched + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! (void)v; // == 84 + //! } + //! @endcode + //! + //! @see stdexec::schedule — the primitive that produces a schedule-sender + //! @see stdexec::starts_on — *begin* execution on a given scheduler + //! @see stdexec::on — run on a different scheduler, then transfer back struct continues_on_t { + //! @brief Construct a sender that runs @c __sndr to completion, then + //! transfers execution to @c __sched before forwarding the + //! completion downstream. + //! + //! @tparam _Scheduler A type satisfying the @c stdexec::scheduler concept. + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! + //! @param __sndr The predecessor sender. Forwarded into the + //! resulting sender. + //! @param __sched The scheduler whose execution resource will host + //! the delivery of @c __sndr's completion. + //! + //! @returns A sender with the same completion signatures as @c __sndr + //! (plus a possible @c set_error_t(std::exception_ptr) for + //! decay-copy failures during the hop). The completions are + //! delivered on @c __sched's execution resource. template constexpr auto operator()(_Sender&& __sndr, _Scheduler __sched) const -> __well_formed_sender auto @@ -393,6 +488,18 @@ namespace STDEXEC schedule_from(static_cast<_Sender&&>(__sndr))); } + //! @brief Construct a sender-adaptor closure that, when applied to a + //! sender, produces continues_on(sndr, __sched). + //! + //! This overload enables the pipe syntax: + //! sndr | continues_on(__sched) is equivalent to + //! continues_on(sndr, __sched). + //! + //! @tparam _Scheduler A type satisfying the @c stdexec::scheduler concept. + //! @param __sched The scheduler to transfer execution to when the + //! closure is later applied to a sender. + //! + //! @returns A sender-adaptor closure object capturing @c __sched. template STDEXEC_ATTRIBUTE(always_inline) constexpr auto operator()(_Scheduler __sched) const noexcept @@ -401,6 +508,13 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c continues_on sender adaptor. + //! + //! @c continues_on is an instance of @ref continues_on_t. See + //! @ref continues_on_t for the full description, completion signatures, + //! and a usage example. + //! + //! @hideinitializer inline constexpr continues_on_t continues_on{}; template <> diff --git a/include/stdexec/__detail/__env.hpp b/include/stdexec/__detail/__env.hpp index 6e56e6e94..3d9aba8e1 100644 --- a/include/stdexec/__detail/__env.hpp +++ b/include/stdexec/__detail/__env.hpp @@ -259,9 +259,75 @@ namespace STDEXEC concept __has_get_env_member = requires { typename __get_env_member_result_t<_EnvProvider>; }; } // namespace __detail - // For getting an execution environment from a receiver or the attributes from a sender. + //! @brief Customization point object that obtains the *environment* of a + //! sender or receiver. + //! + //! Every sender and every receiver has an associated *environment* — an + //! unordered, type-keyed bag of properties such as the stop token, the + //! allocator, the preferred scheduler, the start scheduler, and any + //! domain-specific properties a sender adaptor wants to expose. The + //! environment is what makes the sender model *contextual*: a sender + //! adapts its behavior based on the environment of the receiver it is + //! connected to. + //! + //! @c get_env(provider) returns the environment of @c provider, where + //! @c provider is either a receiver (yielding its environment, which + //! the sender will introspect via queries) or a sender (yielding its + //! *attributes*, which the framework consults to determine things like + //! the sender's completion scheduler). + //! + //! See [exec.queries.get_env] in the C++26 working draft. + //! + //! **Customization.** + //! + //! Most receivers and senders simply expose a @c noexcept, + //! const-callable `.get_env()` member returning their environment: + //! + //! @code{.cpp} + //! struct my_receiver { + //! using receiver_concept = stdexec::receiver_tag; + //! + //! auto get_env() const noexcept { + //! return stdexec::env{stdexec::prop{stdexec::get_stop_token, my_stop_token_}}; + //! } + //! }; + //! @endcode + //! + //! Many receivers don't have any properties to expose — for those, the + //! @c get_env member can simply return an empty @c stdexec::env<> (or + //! the @c get_env CPO will return one automatically via its @c __ignore + //! overload). + //! + //! @c tag_invoke-based customization is supported via a deprecated + //! overload, retained for backwards compatibility. + //! + //! **Environment queries.** + //! + //! Once you have an environment, you query it by calling the appropriate + //! query CPO on it: get_stop_token(env), + //! get_allocator(env), get_scheduler(env), etc. Each + //! query is a separate CPO; the environment dispatches based on the + //! query's type. Inside a sender pipeline you almost always reach for + //! @c stdexec::read_env (or its helpers like @c get_stop_token() with + //! no argument) rather than calling @c get_env directly. + //! + //! @see stdexec::env — the environment container type + //! @see stdexec::read_env — the sender factory that exposes env values to pipelines + //! @see stdexec::get_stop_token — example of an environment query CPO + //! @see stdexec::get_allocator + //! @see stdexec::get_scheduler struct get_env_t { + //! @brief Obtain the environment of @c __env_provider. + //! + //! Dispatches to __env_provider.get_env(), statically + //! asserting that the member is @c noexcept. + //! + //! @tparam _EnvProvider A type whose const-lvalue has a + //! `.get_env() const` member. + //! @param __env_provider The receiver or sender whose environment to + //! retrieve. + //! @returns The environment object (typed as defined by the provider). template requires __detail::__has_get_env_member<_EnvProvider const &> STDEXEC_ATTRIBUTE(nodiscard, always_inline, host, device) @@ -292,6 +358,13 @@ namespace STDEXEC } }; + //! @brief The customization point object for obtaining a sender's or + //! receiver's environment. + //! + //! @c get_env is an instance of @ref get_env_t. See @ref get_env_t for + //! the full description and customization examples. + //! + //! @hideinitializer inline constexpr get_env_t get_env{}; template diff --git a/include/stdexec/__detail/__get_completion_signatures.hpp b/include/stdexec/__detail/__get_completion_signatures.hpp index 1d0fa839e..5435cfb8b 100644 --- a/include/stdexec/__detail/__get_completion_signatures.hpp +++ b/include/stdexec/__detail/__get_completion_signatures.hpp @@ -278,12 +278,90 @@ namespace STDEXEC STDEXEC_PRAGMA_POP() + //! @brief Compute the completion signatures of a sender at compile time. + //! + //! `get_completion_signatures()` is a @c consteval function + //! template that returns the @c stdexec::completion_signatures of + //! @c Sndr — a pack-of-signatures type describing every way @c Sndr + //! can complete (every @c set_value_t, @c set_error_t, and + //! @c set_stopped_t signal it may invoke on its receiver). + //! + //! This is the *type-system half* of the sender protocol — every + //! generic adaptor (@c then, @c when_all, @c sync_wait, …) consults + //! @c get_completion_signatures to know what it has to handle. + //! + //! See [exec.getcomplsigs] in the C++26 working draft. + //! + //! **Two flavors.** + //! + //! @c get_completion_signatures has two forms: + //! + //! - `get_completion_signatures()` — for *environment-independent* + //! senders, which have a single completion-signatures shape no + //! matter what receiver they are connected to. Most senders in + //! practice are environment-independent. + //! - `get_completion_signatures()` — for senders + //! whose completion signatures depend on the receiver's environment. + //! E.g. a scheduler-dependent sender may produce different error + //! completions depending on the actual scheduler queried from + //! @c Env. + //! + //! For convenience, the related concept `sender_in` + //! captures "@c Sndr is a sender whose signatures can be computed in + //! @c Env" — that's the constraint sender adaptors actually use. + //! + //! **Customization.** + //! + //! A sender author provides a @c static @c consteval member template: + //! + //! @code{.cpp} + //! struct my_sender { + //! using sender_concept = stdexec::sender_tag; + //! + //! template + //! static consteval auto get_completion_signatures() noexcept { + //! return stdexec::completion_signatures< + //! stdexec::set_value_t(int), + //! stdexec::set_error_t(std::exception_ptr)>{}; + //! } + //! // ... plus connect() + //! }; + //! @endcode + //! + //! Alternatively, a sender may expose a non-template alias + //! @c completion_signatures (a type alias to a + //! @c stdexec::completion_signatures specialization) — that is the form + //! used by simple senders whose signatures don't depend on the + //! environment. + //! + //! Awaitable types (coroutine-style awaitables) get their completion + //! signatures synthesized automatically: @c set_value_t with the + //! @c co_await result type, @c set_error_t(std::exception_ptr) for + //! any exceptions, and @c set_stopped_t for stop-token cancellation. + //! + //! @see stdexec::completion_signatures — the pack-of-signatures container + //! @see stdexec::completion_signatures_of_t — convenience type alias around this + //! @see stdexec::sender_in — the concept built on top of this + //! @see stdexec::transform_sender — domain-customization run before signature computation template consteval auto get_completion_signatures() { return __cmplsigs::__get_completion_signatures_helper<_Sender>(); } + //! @brief Overload of @ref get_completion_signatures that takes an + //! environment, for senders whose signatures depend on it. + //! + //! @tparam _Sender The sender type. + //! @tparam _Env The receiver's environment type. Must be such that + //! the sender provides a @c get_completion_signatures + //! customization that accepts @c _Env. + //! + //! @returns A @c stdexec::completion_signatures specialization + //! describing every way @c _Sender can complete when + //! connected to a receiver with environment @c _Env. + //! + //! See @ref get_completion_signatures() for the full description. template requires __has_get_completion_signatures<_Sender, _Env> consteval auto get_completion_signatures() diff --git a/include/stdexec/__detail/__into_variant.hpp b/include/stdexec/__detail/__into_variant.hpp index fb039927c..2125e768d 100644 --- a/include/stdexec/__detail/__into_variant.hpp +++ b/include/stdexec/__detail/__into_variant.hpp @@ -109,14 +109,105 @@ namespace STDEXEC }; } // namespace __into_variant + //! @brief A pipeable sender adaptor that collapses a sender's multiple + //! value-completion signatures into a single + //! @c std::variant-of-tuples value completion. + //! + //! @c into_variant takes a sender whose @c set_value_t completion can be + //! one of several shapes — e.g. set_value_t(int) *or* + //! set_value_t(std::string) — and produces a sender that always + //! value-completes with exactly one shape: a single + //! `std::variant, std::tuple, ...>` datum + //! whose alternatives match the input's possible value completions. + //! + //! This is the building block behind @c when_all_with_variant: it lifts + //! any sender into the *single-value-completion* category that + //! @c when_all and @c sync_wait require. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::into_variant(sndr); // direct invocation + //! auto s2 = sndr | stdexec::into_variant(); // pipe syntax (no args) + //! @endcode + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with value-completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs1...) // possibly several + //! set_value_t(Vs2...) + //! set_error_t(Es)... // forwarded unchanged + //! set_stopped_t() // forwarded unchanged + //! @endcode + //! + //! the sender produced by into_variant(sndr) has completion + //! signatures + //! + //! @code{.cpp} + //! set_value_t(std::variant, std::tuple, ...>) + //! set_error_t(Es)... // unchanged + //! set_error_t(std::exception_ptr) // added if variant construction may throw + //! set_stopped_t() // unchanged + //! @endcode + //! + //! When @c sndr value-completes with arguments matching @c Vsi... the + //! resulting sender value-completes with a variant engaged on the + //! corresponding alternative. + //! + //! **Exception behavior.** + //! + //! If constructing the variant alternative (which involves decay-copying + //! the original value arguments) throws, the exception is delivered + //! through @c set_error_t(std::exception_ptr). + //! + //! **Cancellation.** + //! + //! @c into_variant does not interact with the stop token; it only + //! reshapes the value channel. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! + //! // Suppose sndr can value-complete with either int or std::string. + //! auto wrapped = stdexec::into_variant(sndr); + //! auto [v] = stdexec::sync_wait(std::move(wrapped)).value(); + //! // v: std::variant, std::tuple> + //! std::visit([](auto&& tup) { use(tup); }, v); + //! @endcode + //! + //! @see stdexec::when_all_with_variant — applies @c into_variant to each input internally + //! @see stdexec::sync_wait_with_variant — variant-aware top-level wait + //! @see stdexec::sync_wait — requires a single value-completion shape struct into_variant_t { + //! @brief Construct a sender that value-completes with a + //! @c std::variant of the possible value-completion tuples of + //! @c __sndr. + //! + //! @tparam _Sender A type satisfying @c stdexec::sender. + //! @param __sndr The predecessor sender. Forwarded into the result. + //! + //! @returns A sender with a single @c set_value_t completion whose + //! argument is a @c std::variant of `std::tuple` + //! alternatives. template constexpr auto operator()(_Sender&& __sndr) const -> __well_formed_sender auto { return __make_sexpr(__(), static_cast<_Sender&&>(__sndr)); } + //! @brief Construct a sender-adaptor closure that, when applied to a + //! sender, produces into_variant(sndr). + //! + //! This overload enables the pipe syntax: + //! sndr | into_variant() is equivalent to + //! into_variant(sndr). + //! + //! @returns A sender-adaptor closure object. STDEXEC_ATTRIBUTE(always_inline) constexpr auto operator()() const noexcept { @@ -124,6 +215,12 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c into_variant sender adaptor. + //! + //! @c into_variant is an instance of @ref into_variant_t. See + //! @ref into_variant_t for the full description and a usage example. + //! + //! @hideinitializer inline constexpr into_variant_t into_variant{}; template <> diff --git a/include/stdexec/__detail/__just.hpp b/include/stdexec/__detail/__just.hpp index 29a6ce570..455b76f5f 100644 --- a/include/stdexec/__detail/__just.hpp +++ b/include/stdexec/__detail/__just.hpp @@ -85,10 +85,80 @@ namespace STDEXEC }; } // namespace __just + //! @brief A sender factory that produces a sender which completes + //! synchronously with the given values on the value channel. + //! + //! @c just is the simplest sender factory: it captures zero or more values + //! and produces a sender that, when connected and started, immediately + //! delivers those values via @c set_value to the connected receiver — all + //! within the receiver's @c start() call, without any context transition. + //! It is the canonical way to inject literal values into a sender pipeline. + //! + //! @code{.cpp} + //! auto s0 = stdexec::just(); // value-completes with no datums + //! auto s1 = stdexec::just(42); // value-completes with one int + //! auto s2 = stdexec::just(1, 2, 3); // value-completes with three ints + //! auto s3 = stdexec::just(std::string{"x"}, 7); // mixed types are fine + //! @endcode + //! + //! See [exec.just] in the C++26 working draft for the normative specification. + //! + //! **Completion signatures.** + //! + //! Given @c just(ts...) with argument-pack types @c Ts..., the resulting + //! sender has the single completion signature: + //! + //! @code{.cpp} + //! set_value_t(std::decay_t...) + //! @endcode + //! + //! Each argument is decay-copied into the resulting sender. The error and + //! stopped channels are empty — @c just never completes with @c set_error + //! or @c set_stopped. + //! + //! **Exception behavior.** + //! + //! The factory call itself is @c noexcept when every decay-copy is + //! @c noexcept. The produced sender's @c start() is @c noexcept by + //! construction. + //! + //! **Cancellation.** + //! + //! @c just does not consult the receiver's stop token; it always + //! synchronously completes with @c set_value. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto sndr = just(21) | then([](int x) { return x * 2; }); + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == 42); + //! } + //! @endcode + //! + //! @see stdexec::just_error — synchronously complete with an error + //! @see stdexec::just_stopped — synchronously complete with stopped + //! @see stdexec::read_env — synchronously complete with a value read from the environment struct just_t { using __tag_t = set_value_t; + //! @brief Construct a sender that synchronously value-completes with the + //! decay-copies of @c __ts.... + //! + //! @tparam _Ts Zero or more types each satisfying the internal + //! __movable_value concept. + //! @param __ts The values to deliver. Each is decay-copied into the + //! resulting sender. + //! + //! @returns A sender with the single completion signature + //! set_value_t(std::decay_t<_Ts>...). template <__movable_value... _Ts> STDEXEC_ATTRIBUTE(host, device) constexpr auto operator()(_Ts&&... __ts) const noexcept(__nothrow_decay_copyable<_Ts...>) @@ -97,10 +167,58 @@ namespace STDEXEC } }; + //! @brief A sender factory that produces a sender which completes + //! synchronously with the given error on the error channel. + //! + //! @c just_error is the error-channel analogue of @c just: it captures a + //! single error datum and produces a sender that, when connected and + //! started, immediately delivers that error via @c set_error to the + //! connected receiver. It is the canonical way to inject a literal error + //! into a sender pipeline — useful for testing error-handling adaptors + //! such as @c upon_error and @c let_error. + //! + //! @code{.cpp} + //! auto s1 = stdexec::just_error(std::error_code{ENOENT, std::system_category()}); + //! auto s2 = stdexec::just_error(std::make_exception_ptr(std::runtime_error{"boom"})); + //! @endcode + //! + //! See [exec.just] in the C++26 working draft for the normative + //! specification (@c just_error is specified alongside @c just and + //! @c just_stopped). + //! + //! **Completion signatures.** + //! + //! Given just_error(e) with @c E = decltype((e)), the + //! resulting sender has the single completion signature: + //! + //! @code{.cpp} + //! set_error_t(std::decay_t) + //! @endcode + //! + //! The error is decay-copied into the resulting sender. The value and + //! stopped channels are empty. + //! + //! **Cancellation.** + //! + //! @c just_error does not consult the receiver's stop token; it always + //! synchronously completes with @c set_error. + //! + //! @see stdexec::just — synchronously complete with values + //! @see stdexec::just_stopped — synchronously complete with stopped + //! @see stdexec::upon_error — handle the error channel + //! @see stdexec::let_error — handle the error channel with a sender-returning function struct just_error_t { using __tag_t = set_error_t; + //! @brief Construct a sender that synchronously error-completes with the + //! decay-copy of @c __err. + //! + //! @tparam _Error A type satisfying the internal __movable_value concept. + //! @param __err The error datum to deliver. Decay-copied into the sender. + //! + //! @returns A sender with the single completion signature + //! set_error_t(std::decay_t<_Error>). template <__movable_value _Error> STDEXEC_ATTRIBUTE(host, device) constexpr auto operator()(_Error&& __err) const noexcept(__nothrow_decay_copyable<_Error>) @@ -109,10 +227,51 @@ namespace STDEXEC } }; + //! @brief A sender factory that produces a sender which completes + //! synchronously on the stopped channel. + //! + //! @c just_stopped is the stopped-channel analogue of @c just: it produces a + //! sender that, when connected and started, immediately invokes + //! @c set_stopped on the connected receiver. It carries no datum (the + //! stopped channel has none). It is the canonical way to inject a literal + //! cancellation into a sender pipeline — useful for testing cancellation + //! handling adaptors such as @c upon_stopped and @c let_stopped. + //! + //! @code{.cpp} + //! auto s = stdexec::just_stopped(); + //! @endcode + //! + //! See [exec.just] in the C++26 working draft for the normative + //! specification (@c just_stopped is specified alongside @c just and + //! @c just_error). + //! + //! **Completion signatures.** + //! + //! The resulting sender has the single completion signature: + //! + //! @code{.cpp} + //! set_stopped_t() + //! @endcode + //! + //! The value and error channels are empty. + //! + //! **Cancellation.** + //! + //! @c just_stopped does not consult the receiver's stop token (the + //! cancellation it delivers is unconditional, not a response to a request). + //! + //! @see stdexec::just — synchronously complete with values + //! @see stdexec::just_error — synchronously complete with an error + //! @see stdexec::upon_stopped — handle the stopped channel + //! @see stdexec::let_stopped — handle the stopped channel with a sender-returning function struct just_stopped_t { using __tag_t = set_stopped_t; + //! @brief Construct a sender that synchronously stops-completes. + //! + //! @returns A sender with the single completion signature + //! set_stopped_t(). template STDEXEC_ATTRIBUTE(host, device) constexpr auto operator()() const noexcept @@ -133,8 +292,29 @@ namespace STDEXEC struct __sexpr_impl : __just::__impl {}; - inline constexpr just_t just{}; - inline constexpr just_error_t just_error{}; + //! @brief The customization point object for the @c just sender factory. + //! + //! @c just is an instance of @ref just_t. See @ref just_t for the full + //! description, completion signatures, and a usage example. + //! + //! @hideinitializer + inline constexpr just_t just{}; + + //! @brief The customization point object for the @c just_error sender factory. + //! + //! @c just_error is an instance of @ref just_error_t. See @ref just_error_t + //! for the full description, completion signatures, and a usage example. + //! + //! @hideinitializer + inline constexpr just_error_t just_error{}; + + //! @brief The customization point object for the @c just_stopped sender factory. + //! + //! @c just_stopped is an instance of @ref just_stopped_t. See + //! @ref just_stopped_t for the full description, completion signatures, + //! and a usage example. + //! + //! @hideinitializer inline constexpr just_stopped_t just_stopped{}; } // namespace STDEXEC diff --git a/include/stdexec/__detail/__let.hpp b/include/stdexec/__detail/__let.hpp index 201c44dea..93f01cc4d 100644 --- a/include/stdexec/__detail/__let.hpp +++ b/include/stdexec/__detail/__let.hpp @@ -678,24 +678,309 @@ namespace STDEXEC }; } // namespace __let + //! @brief A pipeable sender adaptor that chains a *sender-returning* + //! function onto a predecessor's value completion. + //! + //! @c let_value is the way to launch another asynchronous operation based on + //! the values produced by a predecessor sender. Where @ref then_t "then" + //! takes a function returning a *value*, @c let_value takes a function + //! returning a *sender*, which is then connected and started, becoming the + //! tail of the pipeline. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::let_value(sndr, f); // direct invocation + //! auto s2 = sndr | stdexec::let_value(f); // pipe syntax + //! @endcode + //! + //! The signature of the operator overloads (inherited from a detail base) is: + //! + //! @code{.cpp} + //! template + //! auto operator()(Sender&& sndr, Fun fun) const -> sender auto; // direct + //! + //! template + //! auto operator()(Fun fun) const; // closure + //! @endcode + //! + //! The two forms are expression-equivalent. See [exec.let] in the + //! C++26 working draft for the normative specification. + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // one or more value completions + //! set_error_t(Es)... // forwarded unchanged + //! set_stopped_t() // forwarded unchanged (if present) + //! @endcode + //! + //! the sender produced by let_value(sndr, f) has completion + //! signatures equal to the *union* of: + //! + //! - the completion signatures of every sender returned by an invocation + //! std::invoke(f, vs...) for each value-completion argument pack + //! @c (vs...) of @c sndr, *plus* + //! - the @c set_error_t completions of @c sndr (forwarded unchanged), + //! - the @c set_stopped_t completion of @c sndr (forwarded unchanged), and + //! - @c set_error_t(std::exception_ptr) if invoking @c f or connecting its + //! returned sender may throw. + //! + //! @c f must be invocable with every value-completion argument pack of + //! @c sndr, and every such invocation must return a type satisfying the + //! @c sender concept. Otherwise the program is ill-formed at the point + //! where the resulting sender is connected to a receiver. + //! + //! **Use** @ref then_t "then" **vs.** @c let_value: choose @c then when the + //! callable returns a *value*; choose @c let_value when the callable returns + //! a *sender* (i.e. when the next step is itself asynchronous). Passing a + //! sender-returning function to @c then would forward the sender as a + //! value — almost never what you want. + //! + //! **Exception behavior.** + //! + //! If invoking @c f throws, or if connecting the sender returned by @c f + //! throws, the exception is delivered through + //! @c set_error_t(std::exception_ptr) on the resulting sender. When both + //! steps are @c noexcept, no @c std::exception_ptr error completion is added. + //! + //! **Cancellation.** + //! + //! @c let_value does not introduce stop-token interaction of its own. If + //! @c sndr completes with @c set_stopped, @c f is not invoked and the + //! stopped completion is forwarded. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto fetch_async = [](int id) { + //! return just(id * 10); // pretend this is a non-trivial async op + //! }; + //! + //! auto sndr = just(7) + //! | let_value(fetch_async); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == 70); + //! } + //! @endcode + //! + //! @see stdexec::then — adapt the value channel with a value-returning function + //! @see stdexec::let_error — adapt the error channel with a sender-returning function + //! @see stdexec::let_stopped — adapt the stopped channel with a sender-returning function struct let_value_t : __let::__let_t { using __t = set_value_t; let_value_t() = default; }; + + //! @brief A pipeable sender adaptor that chains a *sender-returning* + //! function onto a predecessor's error completion. + //! + //! @c let_error is to @ref upon_error_t "upon_error" what @ref let_value_t + //! "let_value" is to @ref then_t "then": it lets the recovery step be + //! *another asynchronous operation*. When the predecessor completes with + //! @c set_error_t(e), @c f is invoked with @c e and is expected to return a + //! sender; that sender is connected and started, and its completions become + //! the completions of the overall pipeline. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::let_error(sndr, f); // direct invocation + //! auto s2 = sndr | stdexec::let_error(f); // pipe syntax + //! @endcode + //! + //! See [exec.let] in the C++26 working draft for the normative specification. + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged + //! set_error_t(Es)... // one or more error completions + //! set_stopped_t() // forwarded unchanged (if present) + //! @endcode + //! + //! the sender produced by let_error(sndr, f) has completion + //! signatures equal to the *union* of: + //! + //! - the @c set_value_t completions of @c sndr (forwarded unchanged), + //! - the completion signatures of every sender returned by an invocation + //! std::invoke(f, e) for each error type @c e of @c sndr, + //! - the @c set_stopped_t completion of @c sndr (forwarded unchanged), and + //! - @c set_error_t(std::exception_ptr) if invoking @c f or connecting its + //! returned sender may throw. + //! + //! All original @c set_error_t completions are *consumed*: only errors + //! produced by the senders that @c f returns (or thrown by @c f itself) + //! survive on the error channel. + //! + //! **Use** @ref upon_error_t "upon_error" **vs.** @c let_error: choose + //! @c upon_error when the recovery is synchronous (returns a value); choose + //! @c let_error when the recovery is itself asynchronous (returns a sender). + //! + //! **Exception behavior.** + //! + //! If invoking @c f throws, or if connecting the sender returned by @c f + //! throws, the exception is delivered through + //! @c set_error_t(std::exception_ptr) on the resulting sender. + //! + //! **Cancellation.** + //! + //! @c let_error does not introduce stop-token interaction of its own. A + //! @c set_stopped completion of @c sndr is forwarded without invoking @c f. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto retry_async = [](std::error_code) { return just(7); }; + //! + //! auto sndr = just_error(std::error_code{ENOENT, std::system_category()}) + //! | let_error(retry_async); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == 7); + //! } + //! @endcode + //! + //! @see stdexec::upon_error — adapt the error channel with a value-returning function + //! @see stdexec::let_value — adapt the value channel with a sender-returning function + //! @see stdexec::let_stopped — adapt the stopped channel with a sender-returning function struct let_error_t : __let::__let_t { using __t = set_error_t; let_error_t() = default; }; + + //! @brief A pipeable sender adaptor that chains a *sender-returning* + //! nullary function onto a predecessor's stopped completion. + //! + //! @c let_stopped is to @ref upon_stopped_t "upon_stopped" what + //! @ref let_value_t "let_value" is to @ref then_t "then": it lets the + //! recovery from cancellation be *another asynchronous operation*. When the + //! predecessor completes with @c set_stopped, @c f is invoked with no + //! arguments and is expected to return a sender; that sender is connected + //! and started, and its completions become the completions of the overall + //! pipeline. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::let_stopped(sndr, f); // direct invocation + //! auto s2 = sndr | stdexec::let_stopped(f); // pipe syntax + //! @endcode + //! + //! See [exec.let] in the C++26 working draft for the normative specification. + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged + //! set_error_t(Es)... // forwarded unchanged + //! set_stopped_t() // (must be present) + //! @endcode + //! + //! the sender produced by let_stopped(sndr, f) has completion + //! signatures equal to the *union* of: + //! + //! - the @c set_value_t completions of @c sndr (forwarded unchanged), + //! - the @c set_error_t completions of @c sndr (forwarded unchanged), + //! - the completion signatures of the sender returned by + //! std::invoke(f), and + //! - @c set_error_t(std::exception_ptr) if invoking @c f or connecting its + //! returned sender may throw. + //! + //! The original @c set_stopped_t completion is *consumed*: it appears on + //! the resulting sender only if the sender returned by @c f itself + //! completes via @c set_stopped. + //! + //! **Use** @ref upon_stopped_t "upon_stopped" **vs.** @c let_stopped: choose + //! @c upon_stopped when the recovery is synchronous (returns a value); + //! choose @c let_stopped when the recovery is itself asynchronous + //! (returns a sender). + //! + //! **Exception behavior.** + //! + //! If invoking @c f throws, or if connecting the sender returned by @c f + //! throws, the exception is delivered through + //! @c set_error_t(std::exception_ptr) on the resulting sender. + //! + //! **Cancellation.** + //! + //! @c let_stopped reacts to the predecessor's @c set_stopped; it does not + //! initiate cancellation. The sender returned by @c f sees the receiver's + //! stop token as normal. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto fallback_async = [] { return just(42); }; + //! + //! auto sndr = just_stopped() + //! | let_stopped(fallback_async); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == 42); + //! } + //! @endcode + //! + //! @see stdexec::upon_stopped — adapt the stopped channel with a value-returning function + //! @see stdexec::let_value — adapt the value channel with a sender-returning function + //! @see stdexec::let_error — adapt the error channel with a sender-returning function struct let_stopped_t : __let::__let_t { using __t = set_stopped_t; let_stopped_t() = default; }; - inline constexpr let_value_t let_value{}; - inline constexpr let_error_t let_error{}; + //! @brief The customization point object for the @c let_value sender adaptor. + //! + //! @c let_value is an instance of @ref let_value_t. See @ref let_value_t + //! for the full description, completion-signature transformation rules, + //! exception and cancellation behavior, and a usage example. + //! + //! @hideinitializer + inline constexpr let_value_t let_value{}; + + //! @brief The customization point object for the @c let_error sender adaptor. + //! + //! @c let_error is an instance of @ref let_error_t. See @ref let_error_t + //! for the full description and example. + //! + //! @hideinitializer + inline constexpr let_error_t let_error{}; + + //! @brief The customization point object for the @c let_stopped sender adaptor. + //! + //! @c let_stopped is an instance of @ref let_stopped_t. See @ref let_stopped_t + //! for the full description and example. + //! + //! @hideinitializer inline constexpr let_stopped_t let_stopped{}; template <> diff --git a/include/stdexec/__detail/__on.hpp b/include/stdexec/__detail/__on.hpp index dcf5fc197..8b6768747 100644 --- a/include/stdexec/__detail/__on.hpp +++ b/include/stdexec/__detail/__on.hpp @@ -176,8 +176,117 @@ namespace STDEXEC } // namespace __on //////////////////////////////////////////////////////////////////////////////////////////////// + //! @brief A sender adaptor that runs work on a different scheduler and then + //! transfers execution *back* to the original scheduler. + //! + //! @c on is the "go there, do work, come back" scheduling adaptor. It has + //! two distinct shapes: + //! + //! 1. **Whole-sender form:** on(sched, sndr) — runs the entirety + //! of @c sndr on @c sched's execution resource. When @c sndr completes, + //! execution transfers back to the scheduler that started the operation + //! (the "start scheduler"), and the completion is delivered there. + //! + //! This is the principal difference between @c on and + //! @ref starts_on_t — @c starts_on stays on @c sched; @c on returns home. + //! + //! 2. **Closure-insertion form:** on(sndr, sched, closure) — runs + //! @c sndr on its *current* scheduler, then transfers to @c sched, + //! applies @c closure (a sender-adaptor closure) to the result, runs + //! *that* on @c sched, and finally transfers back to the original + //! completion scheduler. Useful for inserting a CPU-bound transform + //! into an otherwise I/O-bound pipeline (or vice versa) without + //! permanently changing context. + //! + //! This form also has a pipe shorthand: + //! sndr | on(sched, closure). + //! + //! @code{.cpp} + //! // Form 1: run sndr on sched, return to start scheduler. + //! auto s1 = stdexec::on(sched, sndr); + //! + //! // Form 2: run sndr in place, hop to sched for closure, hop back. + //! auto s2 = stdexec::on(sndr, sched, stdexec::then([](int x){ return x*2; })); + //! auto s3 = sndr | stdexec::on(sched, stdexec::then([](int x){ return x*2; })); + //! @endcode + //! + //! See [exec.on] in the C++26 working draft for the normative specification. + //! + //! **The round trip.** + //! + //! What distinguishes @c on from @c starts_on and @c continues_on is the + //! restoration of the original scheduler: + //! + //! | Adaptor | Where work runs | Where completion is delivered | + //! | ---------------------- | --------------- | ------------------------------ | + //! | @c starts_on(sch,s) | on @c sch | on @c sch | + //! | @c continues_on(s,sch) | on @c s's sched | on @c sch | + //! | @c on(sch,s) | on @c sch | on the *start scheduler* | + //! | @c on(s,sch,closure) | mixed (see above) | on the *start scheduler* | + //! + //! The "start scheduler" is read from the receiver's environment via + //! @c get_start_scheduler (form 1) or @c get_completion_scheduler + //! of @c sndr's environment (form 2). + //! + //! **Completion signatures.** + //! + //! Form 1 (on(sched, sndr)): essentially @c sndr's completion + //! signatures, with possible additional @c set_error_t completions from + //! the two scheduling hops. + //! + //! Form 2 (on(sndr, sched, closure)): the completion signatures + //! of closure(continues_on(sndr, sched)) after the final transfer + //! back, again with possible additional @c set_error_t completions from + //! the scheduling hops. + //! + //! If scheduling onto @c sched (or back) fails, an error completion is + //! delivered on an unspecified execution agent. + //! + //! **Cancellation.** + //! + //! Cancellation flows through the scheduling hops normally; a stop + //! request observed between hops typically results in @c set_stopped. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto gpu = get_parallel_scheduler(); // pretend: GPU + //! + //! // Compute on the GPU, but stay on the start scheduler afterwards: + //! auto sndr = + //! just(21) + //! | on(gpu, then([](int x) { return x * 2; })); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! (void)v; // == 42; sync_wait sees the result on its starting context + //! } + //! @endcode + //! + //! @see stdexec::schedule — the primitive that produces a schedule-sender + //! @see stdexec::starts_on — begin on a scheduler and *stay* there + //! @see stdexec::continues_on — transfer to a scheduler *after* a sender completes struct on_t { + //! @brief Form 1: run @c __sndr on @c __sched, then return to the start + //! scheduler. + //! + //! @tparam _Scheduler A type satisfying the @c stdexec::scheduler concept. + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! + //! @param __sched The scheduler whose execution resource will host + //! @c __sndr. + //! @param __sndr The sender to run on @c __sched. + //! + //! @returns A sender that, when connected and started, runs @c __sndr on + //! @c __sched then transfers execution back to the start + //! scheduler (taken from the receiver's environment via + //! @c get_start_scheduler) before forwarding @c __sndr's + //! completion to the receiver. template constexpr auto operator()(_Scheduler&& __sched, _Sender&& __sndr) const -> __well_formed_sender auto @@ -185,6 +294,23 @@ namespace STDEXEC return __make_sexpr(static_cast<_Scheduler&&>(__sched), static_cast<_Sender&&>(__sndr)); } + //! @brief Form 2: run @c __sndr in place, hop to @c __sched, apply + //! @c __clsur there, then hop back. + //! + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! @tparam _Scheduler A type satisfying the @c stdexec::scheduler concept. + //! @tparam _Closure A sender-adaptor closure suitable for chaining + //! onto @c _Sender. + //! + //! @param __sndr The predecessor sender (runs on its own scheduler). + //! @param __sched The scheduler to transition to before applying + //! @c __clsur. + //! @param __clsur The adaptor closure (e.g. then(...), + //! bulk(...)) to apply on @c __sched. + //! + //! @returns A sender that completes on the *original* completion + //! scheduler of @c __sndr, with the result of + //! __clsur(continues_on(__sndr, __sched)). template _Closure> constexpr auto operator()(_Sender&& __sndr, _Scheduler&& __sched, _Closure&& __clsur) const -> __well_formed_sender auto @@ -194,6 +320,17 @@ namespace STDEXEC static_cast<_Sender&&>(__sndr)); } + //! @brief Pipe form of Form 2: construct a sender-adaptor closure that, + //! when applied to a sender, produces + //! on(sndr, __sched, __clsur). + //! + //! @tparam _Scheduler A type satisfying the @c stdexec::scheduler concept. + //! @tparam _Closure A sender-adaptor closure. + //! + //! @param __sched The scheduler to transition to. + //! @param __clsur The adaptor closure to apply on @c __sched. + //! + //! @returns A sender-adaptor closure capturing @c __sched and @c __clsur. template STDEXEC_ATTRIBUTE(always_inline) constexpr auto operator()(_Scheduler&& __sched, _Closure&& __clsur) const @@ -220,6 +357,13 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c on sender adaptor. + //! + //! @c on is an instance of @ref on_t. See @ref on_t for the full + //! description, the distinction between @c on, @c starts_on, and + //! @c continues_on, and usage examples. + //! + //! @hideinitializer inline constexpr on_t on{}; template <> diff --git a/include/stdexec/__detail/__operation_states.hpp b/include/stdexec/__detail/__operation_states.hpp index 20f528c5f..804e3fea4 100644 --- a/include/stdexec/__detail/__operation_states.hpp +++ b/include/stdexec/__detail/__operation_states.hpp @@ -27,7 +27,25 @@ namespace STDEXEC { - // operation state tag type + //! @brief Tag type used to opt a class into the @c stdexec::operation_state + //! concept. + //! + //! A user-defined operation-state type satisfies + //! @c stdexec::operation_state by exposing a public + //! @c operation_state_concept type alias whose type derives from + //! @c operation_state_tag: + //! + //! @code{.cpp} + //! struct my_opstate { + //! using operation_state_concept = stdexec::operation_state_tag; + //! + //! void start() noexcept { ... } + //! }; + //! @endcode + //! + //! @see stdexec::operation_state + //! @see stdexec::sender_tag + //! @see stdexec::receiver_tag struct operation_state_tag {}; @@ -36,8 +54,75 @@ namespace STDEXEC template concept __has_start_member = requires(_Op &__op) { __op.start(); }; + //! @brief Customization point object that begins the execution of an + //! operation state. + //! + //! @c start is the trigger that turns a connected sender into a running + //! asynchronous operation. The operation state returned by + //! @c stdexec::connect does nothing until it is passed to @c start; from + //! that moment on, it is running and will eventually deliver exactly one + //! completion signal to the receiver it was connected with. + //! + //! See [exec.opstate.start] in the C++26 working draft. + //! + //! **Lifetime contract.** + //! + //! The operation state passed to @c start must: + //! + //! 1. Be an *lvalue* — @c start(op) with @c op an rvalue is ill-formed. + //! The reason: the caller is responsible for keeping the operation + //! state alive until completion, which only makes sense for objects + //! with stable identity. + //! 2. Remain alive until the receiver has been completed. The operation + //! state itself typically holds child operation states (for sender + //! adaptors) and references to the receiver's storage — destroying + //! it early would invalidate those. + //! + //! Once @c start returns, the operation is running but may or may not + //! have already completed (an inline-completing operation may have + //! completed synchronously before @c start returned; an async operation + //! may complete arbitrarily later). + //! + //! **Customization.** + //! + //! An operation state opts in by exposing a @c noexcept, + //! @c void-returning `.start()` member: + //! + //! @code{.cpp} + //! struct my_opstate { + //! using operation_state_concept = stdexec::operation_state_tag; + //! + //! // Immovable — once constructed, must stay put: + //! my_opstate(my_opstate&&) = delete; + //! + //! void start() noexcept { + //! // Begin async work; eventually call set_value/set_error/set_stopped + //! // on the receiver stored in this op-state. + //! } + //! }; + //! @endcode + //! + //! `start()` *must* be @c noexcept — there's nowhere for it to throw to, + //! since the caller is typically the runtime, not user code that can + //! handle exceptions. It must also return @c void. The dispatch site + //! enforces both with static asserts. + //! + //! @c tag_invoke-based customization is supported via a deprecated + //! overload, retained for backwards compatibility. + //! + //! @see stdexec::connect — the CPO that produces operation states + //! @see stdexec::operation_state — the concept this CPO drives + //! @see stdexec::set_value — one of the completions @c start eventually triggers struct start_t { + //! @brief Begin execution of @c __op. + //! + //! Dispatches to __op.start(). Statically asserts both + //! @c noexcept and @c void-returning. + //! + //! @tparam _Op A type satisfying @c stdexec::operation_state. + //! @param __op An *lvalue* reference to the operation state. + //! Passing an rvalue is ill-formed. template requires __has_start_member<_Op> STDEXEC_ATTRIBUTE(always_inline) @@ -59,10 +144,61 @@ namespace STDEXEC } }; + //! @brief The customization point object for starting an operation state. + //! + //! @c start is an instance of @ref start_t. See @ref start_t for the + //! full description, the lifetime contract, and customization examples. + //! + //! @hideinitializer inline constexpr start_t start{}; ///////////////////////////////////////////////////////////////////////////// // [execution.op_state] + + //! @brief An in-progress, *immovable*, *startable* representation of an + //! asynchronous operation — the result of connecting a sender to + //! a receiver. + //! + //! An *operation state* is what you get from @c stdexec::connect: it is the + //! concrete, type-erased-by-construction record of one specific sender + //! being driven into one specific receiver. Calling @c stdexec::start on + //! it begins the work; the operation runs until it eventually invokes + //! one of @c set_value / @c set_error / @c set_stopped on the receiver + //! it was constructed with. + //! + //! Three properties define the concept @c operation_state: + //! + //! 1. The type is destructible — operations clean up cleanly when their + //! storage goes away (typically after they have completed). + //! 2. The type is an object type (not a reference, not a function) — + //! operation states are *stored*, not passed by handle. + //! 3. @c stdexec::start(op) is well-formed for any lvalue @c op of the + //! type. + //! + //! What the concept does *not* require (but the rules of the sender + //! model do): + //! + //! - **Immovability after construction.** Once an operation state has + //! been constructed, it must not be moved or copied — child operations + //! inside it typically hold pointers into its storage, which would + //! dangle on move. The natural way to satisfy this is to delete the + //! move and copy constructors, which is what most operation states do. + //! The concept itself doesn't check this; the rule is part of the + //! sender-model contract. + //! - **Lifetime guarantee until completion.** Once @c start has been + //! called, the operation state must remain alive until the receiver + //! has been completed. This is the caller's responsibility (the + //! caller of @c start, typically a sender adaptor or a consumer). + //! + //! Like receivers, operation states are usually an implementation detail + //! of sender adaptors and consumers — most user code never names a + //! specific operation-state type. + //! + //! See [exec.opstate] in the C++26 working draft. + //! + //! @see stdexec::connect — the customization point that produces operation states + //! @see stdexec::start — the customization point this concept depends on + //! @see stdexec::operation_state_tag — the tag type that opts a class into this concept template concept operation_state = __std::destructible<_Op> && std::is_object_v<_Op> && requires(_Op &__op) { STDEXEC::start(__op); }; diff --git a/include/stdexec/__detail/__read_env.hpp b/include/stdexec/__detail/__read_env.hpp index 4361edaa4..b0c88e998 100644 --- a/include/stdexec/__detail/__read_env.hpp +++ b/include/stdexec/__detail/__read_env.hpp @@ -151,6 +151,81 @@ namespace STDEXEC } }; + //! @brief A sender factory that produces a sender whose value completion is + //! the result of querying the receiver's environment. + //! + //! @c read_env reaches *into* the receiver to read a value associated with + //! a *query CPO* — things like the receiver's stop token, its associated + //! allocator, or its preferred scheduler. The resulting sender, when + //! connected and started, evaluates @c q(get_env(rcvr)) and delivers the + //! result via @c set_value to the connected receiver. + //! + //! It is the primitive used by the standard environment-query helpers + //! such as @c get_stop_token(), @c get_allocator(), @c get_scheduler(), + //! and @c get_delegation_scheduler() — each of those is simply + //! @c read_env applied to the corresponding query CPO. + //! + //! The call form takes a *query CPO* (not a value): + //! + //! @code{.cpp} + //! auto sndr = stdexec::read_env(stdexec::get_stop_token); + //! @endcode + //! + //! See [exec.read.env] in the C++26 working draft for the normative + //! specification. + //! + //! **Completion signatures.** + //! + //! Given read_env(q) and an environment type @c Env (taken from + //! the connected receiver), the resulting sender has completion signatures: + //! + //! @code{.cpp} + //! set_value_t(decltype(q(declval()))) // always present + //! set_error_t(std::exception_ptr) // present iff q(env) may throw + //! @endcode + //! + //! The query result type is taken from the *actual* environment at + //! connect time, so the same @c read_env sender may have different + //! concrete completion signatures depending on which receiver it is + //! connected to. + //! + //! If the environment does not provide a value for @c q (i.e. + //! q(env) is ill-formed or returns @c void), the program is + //! ill-formed at the point where the sender is connected, with a + //! diagnostic that names the offending query. + //! + //! **Exception behavior.** + //! + //! If invoking @c q on the receiver's environment throws, the exception + //! is delivered through @c set_error_t(std::exception_ptr). If @c q is + //! @c noexcept (typical for query CPOs), no @c std::exception_ptr error + //! completion is added. + //! + //! **Cancellation.** + //! + //! @c read_env does not consult the receiver's stop token; it completes + //! synchronously in its @c start. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! using namespace stdexec; + //! + //! // Lift the current stop token into the pipeline so a downstream + //! // algorithm can inspect it: + //! auto sndr = + //! read_env(get_stop_token) + //! | then([](auto tok) { + //! return tok.stop_requested(); + //! }); + //! @endcode + //! + //! @see stdexec::just — synchronously complete with literal values + //! @see stdexec::get_stop_token — equivalent to read_env(get_stop_token) + //! @see stdexec::get_scheduler — equivalent to read_env(get_scheduler) + //! + //! @hideinitializer inline constexpr __read_env_t read_env{}; template <> diff --git a/include/stdexec/__detail/__receivers.hpp b/include/stdexec/__detail/__receivers.hpp index 5887af74b..8c317c7e3 100644 --- a/include/stdexec/__detail/__receivers.hpp +++ b/include/stdexec/__detail/__receivers.hpp @@ -52,11 +52,58 @@ namespace STDEXEC static_cast<_Receiver &&>(__rcvr).set_value(static_cast<_As &&>(__args)...); }; + //! @brief Customization point object for the *value* completion signal of + //! the sender/receiver protocol. + //! + //! `set_value(rcvr, vs...)` is the call an operation state makes on its + //! receiver to deliver a successful asynchronous result. It is one of + //! three terminal completion signals (alongside @ref set_error_t and + //! @ref set_stopped_t) that exactly one of will be called on a receiver + //! once a connected operation has started. + //! + //! User code rarely calls `set_value` directly — it is invoked from + //! inside operation-state implementations. Sender authors writing new + //! adaptors do call it, and *receiver* authors provide the matching + //! member that this CPO dispatches to. + //! + //! **Customization.** + //! + //! A receiver opts into receiving value completions by defining a + //! @c noexcept, @c void-returning member: + //! + //! @code{.cpp} + //! struct my_receiver { + //! using receiver_concept = stdexec::receiver_tag; + //! void set_value(int v) noexcept { ... } // overload set per value type + //! }; + //! @endcode + //! + //! At the call site, stdexec::set_value(rcvr, vs...) dispatches + //! to rcvr.set_value(vs...), statically asserting both that the + //! member is @c noexcept and that it returns @c void. + //! + //! See [exec.recv] in the C++26 working draft. + //! + //! @see stdexec::set_error — the error-completion CPO + //! @see stdexec::set_stopped — the stopped-completion CPO + //! @see stdexec::receiver — the receiver concept this CPO drives + //! @see stdexec::receiver_of — receiver plus specific completion signatures struct set_value_t : __detail::__completion_tag<__disposition::__value> { template using __f = __minvoke<_Fn, _As...>; + //! @brief Deliver a value completion to @c __rcvr. + //! + //! Dispatches to __rcvr.set_value(__as...). The static + //! asserts inside enforce that the member is @c noexcept and that it + //! returns @c void — the two non-negotiable properties of every + //! completion signal. + //! + //! @tparam _Receiver A type whose decayed form satisfies + //! @c stdexec::receiver and has a matching + //! `.set_value(_As...)` member. + //! @tparam _As The value-datum argument types. template requires __set_value_member<_Receiver, _As...> STDEXEC_ATTRIBUTE(host, device, always_inline) @@ -89,12 +136,53 @@ namespace STDEXEC static_cast<_Receiver &&>(__rcvr).set_error(static_cast<_Error &&>(__err)); }; + //! @brief Customization point object for the *error* completion signal of + //! the sender/receiver protocol. + //! + //! `set_error(rcvr, e)` is the call an operation state makes on its + //! receiver to deliver a failure. Unlike a thrown exception, the error + //! is a *typed datum* — receivers may distinguish, say, + //! @c std::exception_ptr from @c std::error_code from a domain-specific + //! error enum by overloading on the argument type. + //! + //! **Customization.** + //! + //! A receiver opts into receiving error completions of a given type @c E + //! by defining a @c noexcept, @c void-returning member: + //! + //! @code{.cpp} + //! struct my_receiver { + //! using receiver_concept = stdexec::receiver_tag; + //! void set_error(std::exception_ptr e) noexcept { ... } + //! void set_error(std::error_code e) noexcept { ... } // multiple OK + //! }; + //! @endcode + //! + //! Like @c set_value, the dispatch site enforces @c noexcept and + //! @c void return via static asserts. + //! + //! **Receivers MUST accept exactly one error completion at runtime.** + //! That is: at most one of @c set_value, @c set_error, @c set_stopped + //! is ever called on a given receiver, exactly once. + //! + //! See [exec.recv] in the C++26 working draft. + //! + //! @see stdexec::set_value + //! @see stdexec::set_stopped + //! @see stdexec::receiver struct set_error_t : __detail::__completion_tag<__disposition::__error> { template requires(sizeof...(_Args) == 1) using __f = __minvoke<_Fn, _Args...>; + //! @brief Deliver an error completion to @c __rcvr. + //! + //! Dispatches to __rcvr.set_error(__err). Statically asserts + //! both @c noexcept and @c void-returning. + //! + //! @tparam _Receiver A type with a matching `.set_error(_Error)` member. + //! @tparam _Error The error datum type. template requires __set_error_member<_Receiver, _Error> STDEXEC_ATTRIBUTE(host, device, always_inline) @@ -127,12 +215,50 @@ namespace STDEXEC static_cast<_Receiver &&>(__rcvr).set_stopped(); }; + //! @brief Customization point object for the *stopped* completion signal + //! of the sender/receiver protocol. + //! + //! `set_stopped(rcvr)` is the call an operation state makes on its + //! receiver to report that the operation was cancelled. It carries + //! *no datum* — the stopped channel is informational only ("we are + //! ending early because cancellation was requested or because no result + //! is needed any more"). + //! + //! Cancellation is *cooperative*: receivers can request stop via the + //! stop token in their environment (see @c stdexec::get_stop_token); + //! senders that observe such a request may complete with + //! @c set_stopped instead of @c set_value or @c set_error. + //! + //! **Customization.** + //! + //! A receiver opts into receiving stopped completions by defining a + //! @c noexcept, @c void-returning *nullary* member: + //! + //! @code{.cpp} + //! struct my_receiver { + //! using receiver_concept = stdexec::receiver_tag; + //! void set_stopped() noexcept { ... } + //! }; + //! @endcode + //! + //! See [exec.recv] in the C++26 working draft. + //! + //! @see stdexec::set_value + //! @see stdexec::set_error + //! @see stdexec::get_stop_token — the receiver-environment query for the stop token struct set_stopped_t : __detail::__completion_tag<__disposition::__stopped> { template requires(sizeof...(_Args) == 0) using __f = __minvoke<_Fn, _Args...>; + //! @brief Deliver a stopped completion to @c __rcvr. + //! + //! Dispatches to __rcvr.set_stopped(). Statically asserts both + //! @c noexcept and @c void-returning. + //! + //! @tparam _Receiver A type with a matching nullary + //! `.set_stopped()` member. template requires __set_stopped_member<_Receiver> STDEXEC_ATTRIBUTE(host, device, always_inline) @@ -156,10 +282,48 @@ namespace STDEXEC } }; - inline constexpr set_value_t set_value{}; - inline constexpr set_error_t set_error{}; + //! @brief The customization point object for delivering a value completion. + //! + //! @c set_value is an instance of @ref set_value_t. See @ref set_value_t + //! for the full description and customization rules. + //! + //! @hideinitializer + inline constexpr set_value_t set_value{}; + + //! @brief The customization point object for delivering an error completion. + //! + //! @c set_error is an instance of @ref set_error_t. See @ref set_error_t + //! for the full description and customization rules. + //! + //! @hideinitializer + inline constexpr set_error_t set_error{}; + + //! @brief The customization point object for delivering a stopped completion. + //! + //! @c set_stopped is an instance of @ref set_stopped_t. See + //! @ref set_stopped_t for the full description and customization rules. + //! + //! @hideinitializer inline constexpr set_stopped_t set_stopped{}; + //! @brief Tag type used to opt a class into the @c stdexec::receiver concept. + //! + //! A user-defined type satisfies @c stdexec::receiver by exposing a public + //! @c receiver_concept type alias whose type derives from @c receiver_tag: + //! + //! @code{.cpp} + //! struct my_receiver { + //! using receiver_concept = stdexec::receiver_tag; + //! + //! void set_value(int v) noexcept { ... } + //! void set_error(std::exception_ptr e) noexcept { ... } + //! void set_stopped() noexcept { ... } + //! }; + //! @endcode + //! + //! @see stdexec::receiver + //! @see stdexec::sender_tag + //! @see stdexec::operation_state_tag struct receiver_tag { using receiver_concept = receiver_tag; // NOT TO SPEC @@ -175,6 +339,40 @@ namespace STDEXEC } &&) __std::derived_from); } // namespace __detail + //! @brief The fundamental concept of the receiver model: a callback-shaped + //! object that consumes the result of an asynchronous operation. + //! + //! A *receiver* is the destination half of a sender/receiver pair. It is + //! the object on which a started operation eventually invokes one of + //! @c set_value, @c set_error, or @c set_stopped to deliver its + //! completion. Receivers are typically synthesized by sender consumers + //! and adaptors — most user code never writes a receiver by hand; it + //! writes senders and composes them. + //! + //! Concretely, a type @c R satisfies @c receiver if: + //! + //! 1. @c R is opted into the concept via a @c receiver_concept type + //! alias deriving from @c stdexec::receiver_tag. + //! 2. @c R provides an environment via @c stdexec::get_env (so child + //! operations can query for the stop token, allocator, scheduler, + //! etc.). + //! 3. @c R's decayed type is @em nothrow move-constructible — receivers + //! are moved into operation states by sender adaptors, and that move + //! must not throw. + //! 4. @c R's decayed type is constructible from an @c R. + //! + //! Note that this concept alone does *not* require @c R to accept any + //! particular completion signals — for that, see @c receiver_of, which + //! takes a @c completion_signatures pack and validates that the receiver + //! has matching @c set_value / @c set_error / @c set_stopped members. + //! + //! See [exec.recv.concepts] in the C++26 working draft. + //! + //! @see stdexec::receiver_of — receiver plus specific completion signatures + //! @see stdexec::receiver_tag — the tag type that opts a class into this concept + //! @see stdexec::set_value + //! @see stdexec::set_error + //! @see stdexec::set_stopped template concept receiver = __detail::__enable_receiver<__decay_t<_Receiver>> && __environment_provider<__cref_t<_Receiver>> @@ -204,6 +402,28 @@ namespace STDEXEC __detail::__try_completion<__decay_t<_Receiver>>(static_cast<_Sigs *>(nullptr)))); } // namespace __detail + //! @brief A @c receiver that accepts a specific set of completion + //! signatures. + //! + //! `receiver_of` says: "R is a receiver, and for every + //! @c set_xxx_t(Args...) signature in @c Sigs (a + //! @c stdexec::completion_signatures pack), R has a matching member that + //! is callable with @c Args... ". This is the constraint that ensures a + //! sender's completion signals can actually be delivered to the + //! receiver — sender adaptors typically express their compatibility + //! requirements in terms of @c receiver_of, not bare @c receiver. + //! + //! When this concept fails, stdexec produces a focused error message + //! naming the @em specific completion signal the receiver doesn't + //! accept (e.g. "the receiver does not accept set_value_t(int)") — + //! this is the main reason to use @c receiver_of over manually checking + //! each member's callability. + //! + //! See [exec.recv.concepts] in the C++26 working draft. + //! + //! @see stdexec::receiver — without the signature check + //! @see stdexec::sender_to — the sender-side mirror of this concept + //! @see stdexec::completion_signatures — the signature pack this concept consumes template concept receiver_of = receiver<_Receiver> && requires(_Completions *__completions) { { __detail::__try_completions<_Receiver>(__completions) } -> __ok; diff --git a/include/stdexec/__detail/__schedulers.hpp b/include/stdexec/__detail/__schedulers.hpp index ae52f0ab9..d019455da 100644 --- a/include/stdexec/__detail/__schedulers.hpp +++ b/include/stdexec/__detail/__schedulers.hpp @@ -43,8 +43,94 @@ namespace STDEXEC static_cast<_Scheduler &&>(__sched).schedule(); }; + //! @brief A sender factory that obtains a sender from a scheduler. + //! + //! @c schedule is the bridge between a *scheduler* (a lightweight handle + //! to an execution context) and the *sender* world: it produces a sender + //! that, when connected and started, eventually completes with @c set_value + //! to the connected receiver *from the context of the scheduler*. It is + //! the canonical way to begin a sender pipeline that needs to run on a + //! specific execution context (a thread pool, a GPU stream, an event loop, + //! …). + //! + //! @code{.cpp} + //! auto sched = stdexec::get_parallel_scheduler(); + //! auto sndr = stdexec::schedule(sched); + //! @endcode + //! + //! The sender returned by @c schedule(sched) is informally called a + //! "schedule-sender" or "schedule sender". The expression + //! schedule(sch) is *expression-equivalent* to + //! sch.schedule(), which is the customization point that + //! scheduler authors implement. + //! + //! @c schedule also drives the @c stdexec::scheduler concept: a type @c S + //! satisfies @c scheduler if (and roughly only if) @c schedule(s) is a + //! valid expression returning a sender, and @c S is equality-comparable + //! and nothrow-move-constructible. + //! + //! See [exec.schedule] in the C++26 working draft for the normative + //! specification. + //! + //! **Completion signatures.** + //! + //! The exact signatures are determined by the scheduler's implementation, + //! but every conforming schedule-sender includes: + //! + //! @code{.cpp} + //! set_value_t() // delivered on the scheduler's context + //! set_stopped_t() // typical: stop-token observed during scheduling + //! set_error_t(...) // implementation-defined; some schedulers can fail + //! @endcode + //! + //! @c set_value carries no datums. The point of @c schedule is the + //! *context transition*, not the value — downstream adaptors (@c then, + //! @c let_value, etc.) chained onto the schedule-sender therefore run on + //! the scheduler's context. + //! + //! **Cancellation.** + //! + //! A schedule-sender that has not yet started executing on the scheduler's + //! context typically observes the receiver's stop token and may complete + //! with @c set_stopped instead. Once it has begun running on the + //! scheduler's context, the value completion is delivered. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! auto sched = get_parallel_scheduler(); + //! + //! auto sndr = + //! schedule(sched) // hop onto sched + //! | then([] { return 42; }); // ... and compute on it + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! (void)v; + //! } + //! @endcode + //! + //! @see stdexec::starts_on — start a sender on a given scheduler + //! @see stdexec::continues_on — transfer execution to a scheduler mid-pipeline + //! @see stdexec::on — execute a sender on a scheduler then return struct schedule_t { + //! @brief Obtain a schedule-sender by calling @c __sched.schedule(). + //! + //! @tparam _Scheduler A type whose lvalue has a member function + //! @c .schedule() returning a sender. + //! + //! @param __sched The scheduler to obtain a sender from. + //! + //! @returns The sender produced by __sched.schedule() — a + //! sender that, when connected and started, value-completes + //! (with no datums) on @c __sched's execution context. + //! + //! @pre decltype(__sched.schedule()) must satisfy the + //! @c stdexec::sender concept (statically checked). template requires __has_schedule_member<_Scheduler> STDEXEC_ATTRIBUTE(host, device, always_inline) @@ -57,6 +143,13 @@ namespace STDEXEC return static_cast<_Scheduler &&>(__sched).schedule(); } + //! @brief Deprecated overload: obtain a schedule-sender via @c tag_invoke. + //! + //! @deprecated The @c tag_invoke-based customization of @c schedule is + //! deprecated in favor of the sched.schedule() + //! member-function form. New scheduler types should provide + //! a member @c .schedule() instead of a @c tag_invoke + //! overload for @c schedule_t. template requires __has_schedule_member<_Scheduler> || __tag_invocable [[deprecated("the use of tag_invoke for schedule is deprecated")]] @@ -70,10 +163,46 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c schedule sender factory. + //! + //! @c schedule is an instance of @ref schedule_t. See @ref schedule_t for + //! the full description, completion signatures, scheduler-concept + //! relationship, and a usage example. + //! + //! @hideinitializer inline constexpr schedule_t schedule{}; ///////////////////////////////////////////////////////////////////////////// // [exec.sched] + + //! @brief A lightweight handle to an *execution context* — the + //! abstraction over things that can run sender pipelines. + //! + //! A *scheduler* is a small, value-like handle to a (potentially heavy + //! and immovable) *execution resource*: a thread pool, a GPU stream, an + //! event loop, an inline run-loop, etc. The only operation a scheduler + //! must support is @c stdexec::schedule, which obtains a sender that, when + //! connected and started, eventually value-completes on that resource. + //! + //! Concretely, a type @c S satisfies @c scheduler if: + //! + //! 1. @c schedule(s) is well-formed and returns a @c sender. This is + //! the *defining* operation; everything else is value-semantics + //! plumbing. + //! 2. @c S's decayed type is equality-comparable (two schedulers compare + //! equal iff they refer to the same execution resource — used for + //! optimization decisions such as elision of redundant + //! @c continues_on hops). + //! 3. @c S's decayed type is copy-constructible and + //! @em nothrow move-constructible — schedulers are cheap to pass + //! around. + //! + //! See [exec.sched] in the C++26 working draft. + //! + //! @see stdexec::schedule — the customization point that defines this concept + //! @see stdexec::starts_on — adaptor that runs a sender on a scheduler + //! @see stdexec::continues_on — adaptor that transfers to a scheduler mid-pipeline + //! @see stdexec::schedule_result_t — the sender type returned by @c schedule(s) template concept scheduler = __callable // && __std::equality_comparable<__decay_t<_Scheduler>> diff --git a/include/stdexec/__detail/__scope_concepts.hpp b/include/stdexec/__detail/__scope_concepts.hpp index b6308da59..5879d507e 100644 --- a/include/stdexec/__detail/__scope_concepts.hpp +++ b/include/stdexec/__detail/__scope_concepts.hpp @@ -32,6 +32,39 @@ namespace STDEXEC { ///////////////////////////////////////////////////////////////////////////// // [exec.scope.concepts] + + //! @brief A movable handle representing successful (or failed) + //! registration of an operation with an async scope. + //! + //! Implementations of @c stdexec::scope_token return a + //! @c scope_association from try_associate(). It is a small, + //! movable value that: + //! + //! - Contextually converts to @c bool to indicate whether the + //! association succeeded (the scope is still open and the operation + //! was registered) or failed (the scope is shutting down and the + //! operation must not start). + //! - Holds onto whatever state the scope needs to track the operation + //! so that the operation can be deregistered at completion. + //! - Can produce a fresh, equivalent association via + //! try_associate() — used to model "re-associate with the + //! same scope". + //! + //! Concretely, @c scope_association requires the type to be movable, + //! nothrow move-constructible and assignable, default-initializable, + //! and to support both the @c bool conversion and the + //! @c try_associate() member. + //! + //! User code typically does not interact with @c scope_association + //! directly — it is the *return type* of @c scope_token::try_associate + //! and is used internally by @c stdexec::spawn and + //! @c stdexec::spawn_future. + //! + //! See [exec.scope.concepts] in the C++26 working draft. + //! + //! @see stdexec::scope_token + //! @see stdexec::spawn + //! @see stdexec::spawn_future template concept scope_association = __std::movable<_Assoc> && __nothrow_move_constructible<_Assoc> && __nothrow_move_assignable<_Assoc> @@ -70,6 +103,35 @@ namespace STDEXEC }; } // namespace __scope_concepts + //! @brief A copyable handle to an *async scope* — the owner of lifetime + //! for spawned operations. + //! + //! An async scope is a logical container for fire-and-forget operations + //! launched via @c stdexec::spawn or @c stdexec::spawn_future. It tracks + //! every operation associated with it so that, at shutdown, it can + //! block until every spawned operation has completed (typically via a + //! @c .join() member that returns a sender). + //! + //! A @c scope_token is a small, copyable handle to such a scope. User + //! code typically obtains a token from an @c exec::async_scope via + //! scope.get_token() and passes it into @c spawn or + //! @c spawn_future. + //! + //! Concretely, a type @c T satisfies @c scope_token if it is copyable + //! and provides two members: + //! + //! 1. token.try_associate() — attempts to register a new + //! operation with the scope, returning a @c scope_association whose + //! boolean conversion indicates success. Fails (returns "false") + //! when the scope has already begun shutting down. + //! 2. token.wrap(sndr) — wraps a sender so that, when started + //! via @c spawn, its lifetime is tied to the scope. + //! + //! See [exec.scope.concepts] in the C++26 working draft. + //! + //! @see stdexec::scope_association — the return type of @c try_associate + //! @see stdexec::spawn — fire-and-forget into a scope + //! @see stdexec::spawn_future — spawn into a scope and observe via a sender template concept scope_token = __std::copyable<_Token> && requires(_Token const __token) { { __token.try_associate() } -> scope_association; diff --git a/include/stdexec/__detail/__sender_concepts.hpp b/include/stdexec/__detail/__sender_concepts.hpp index 04e1c7984..62f28bd6b 100644 --- a/include/stdexec/__detail/__sender_concepts.hpp +++ b/include/stdexec/__detail/__sender_concepts.hpp @@ -33,6 +33,23 @@ namespace STDEXEC { ///////////////////////////////////////////////////////////////////////////// // [execution.senders] + + //! @brief Tag type used to opt a class into the @c stdexec::sender concept. + //! + //! A user-defined type satisfies @c stdexec::sender by exposing a public + //! @c sender_concept type alias whose type derives from @c sender_tag: + //! + //! @code{.cpp} + //! struct my_sender { + //! using sender_concept = stdexec::sender_tag; + //! + //! // ... usual sender machinery: completion signatures, connect ... + //! }; + //! @endcode + //! + //! @see stdexec::sender + //! @see stdexec::receiver_tag + //! @see stdexec::operation_state_tag struct sender_tag { // NOT TO SPEC: @@ -46,10 +63,63 @@ namespace STDEXEC || __awaitable<_Sender, __detail::__promise>>; } // namespace __detail + //! @brief A variable template that opts a class into the @c stdexec::sender + //! concept by an alternative path. + //! + //! Specialize `enable_sender` to @c true to declare that + //! @c MySender is a sender, *without* having to define a + //! @c sender_concept type alias on the class itself. This is useful + //! when the class cannot be modified (e.g. third-party types) or when + //! the class is a coroutine awaitable type. + //! + //! @code{.cpp} + //! struct legacy_sender { }; // cannot be modified + //! + //! template <> + //! inline constexpr bool stdexec::enable_sender = true; + //! @endcode + //! + //! By default, `enable_sender` is @c true when @c S has a + //! @c sender_concept alias deriving from @c sender_tag, *or* when @c S + //! is awaitable in stdexec's coroutine promise type. template inline constexpr bool enable_sender = __detail::__enable_sender<_Sender>; // [exec.snd.concepts] + + //! @brief The fundamental concept of the sender model: a type that + //! describes (but does not yet execute) an asynchronous operation. + //! + //! A @c sender is the basic unit of composition in stdexec. It is a value + //! type that *describes* an async computation; the work it describes + //! does not start until the sender is *connected* to a receiver (via + //! @c stdexec::connect) and the resulting *operation state* is started + //! (via @c stdexec::start). + //! + //! Concretely, a type @c S satisfies @c sender if: + //! + //! 1. @c S has been opted into the concept — either by exposing a + //! @c sender_concept type alias derived from @c stdexec::sender_tag, + //! or by specializing `stdexec::enable_sender` to @c true, or by + //! being an awaitable in stdexec's coroutine promise type. + //! 2. @c S provides an environment via @c stdexec::get_env (every sender + //! has an environment, possibly empty). + //! 3. @c S's decayed type is move-constructible and constructible from + //! an @c S (this is what allows senders to be stored and forwarded + //! by value). + //! + //! Note that @c sender by itself does *not* require the sender's + //! completion signatures to be computable. That is the additional + //! constraint of @c sender_in (which carries an environment). Generic + //! sender-adaptor code that needs to know "what does this sender + //! complete with?" uses `sender_in`, not @c sender (alone). + //! + //! See [exec.snd.concepts] in the C++26 working draft. + //! + //! @see stdexec::sender_in — sender plus a specific environment, with computable signatures + //! @see stdexec::sender_to — sender plus a specific receiver, with compatible signatures + //! @see stdexec::sender_tag — the tag type that opts a class into this concept + //! @see stdexec::enable_sender — alternative opt-in path template concept sender = enable_sender<__decay_t<_Sender>> // && __environment_provider<__cref_t<_Sender>> // @@ -66,6 +136,33 @@ namespace STDEXEC __valid_completion_signatures; #endif + //! @brief A @c sender whose *completion signatures* can be computed in a + //! given environment. + //! + //! @c sender_in is the form of the sender concept that generic adaptor + //! code actually uses. Where @c sender just asks "is this a sender at + //! all?", `sender_in` asks "is @c S a sender whose completion + //! signatures we can compute when connected to a receiver with + //! environment @c Env?" — that information is what every adaptor needs + //! to type-check itself. + //! + //! Concretely, `sender_in` requires: + //! + //! 1. @c S satisfies @c sender. + //! 2. `get_completion_signatures()` is a constant + //! expression whose value is a valid + //! @c completion_signatures specialization. + //! + //! The @c Env parameter is optional (the variadic accepts zero or one + //! environment). When no environment is supplied, the sender must have + //! a *dependent-environment-free* set of completion signatures — i.e. + //! its signatures must not vary by environment. + //! + //! See [exec.snd.concepts] in the C++26 working draft. + //! + //! @see stdexec::sender — the base concept + //! @see stdexec::sender_to — adds a specific receiver + //! @see stdexec::get_completion_signatures — the customization point this concept depends on template concept sender_in = (sizeof...(_Env) <= 1) // @@ -83,6 +180,22 @@ namespace STDEXEC && sender_in<_Sender, env_of_t<_Receiver>> // && __receiver_from<_Receiver, _Sender>; + //! @brief A @c sender that can be connected to a specific @c receiver. + //! + //! `sender_to` is the strongest form of the sender concept: it + //! requires that @c S is a sender whose completion signatures can be + //! computed in @c R's environment, that @c R is a receiver that accepts + //! all of those signatures, *and* that @c connect(S, R) is well-formed. + //! + //! This is the constraint a sender consumer or scheduler implementation + //! uses just before actually calling @c connect — it's the strongest + //! way to say "yes, this pair is wired up correctly." + //! + //! See [exec.snd.concepts] in the C++26 working draft. + //! + //! @see stdexec::sender_in — without the receiver-compatibility check + //! @see stdexec::receiver_of — the receiver-side mirror of this concept + //! @see stdexec::connect — the operation @c sender_to validates template concept sender_to = __sender_to<_Sender, _Receiver> // && requires(_Sender &&__sndr, _Receiver &&__rcvr) { diff --git a/include/stdexec/__detail/__spawn.hpp b/include/stdexec/__detail/__spawn.hpp index 168625e21..577296a80 100644 --- a/include/stdexec/__detail/__spawn.hpp +++ b/include/stdexec/__detail/__spawn.hpp @@ -123,6 +123,71 @@ namespace STDEXEC } }; + //! @brief A sender consumer that eagerly starts a sender and ties its + //! lifetime to an *async scope*. + //! + //! @c spawn is the standard "fire-and-forget into a scope" consumer. + //! You give it a sender, a @c scope_token (a handle to an async scope), + //! and optionally an environment, and @c spawn: + //! + //! 1. allocates an operation state on the heap (using an allocator + //! queried from the environment or the sender's own environment), + //! 2. tries to associate the resulting operation with the scope via + //! token.try_associate(), + //! 3. if the association succeeds, eagerly @c start s the operation, + //! and on completion deallocates the state and releases the scope + //! association. + //! + //! If association fails (typically because the scope has already begun + //! shutting down), @c spawn destroys the state and returns without + //! starting the operation. The result of the sender, if any, is + //! discarded — @c spawn returns @c void. + //! + //! See [exec.spawn] in the C++26 working draft for the normative + //! specification. + //! + //! @code{.cpp} + //! exec::async_scope scope; + //! + //! stdexec::spawn(stdexec::just(42) | stdexec::then([](int x) { + //! std::println("background work produced {}", x); + //! }), scope.get_token()); + //! + //! // Later, before destroying scope: + //! stdexec::sync_wait(scope.join()); + //! @endcode + //! + //! **Completion requirements.** + //! + //! The argument sender must not be able to complete with @c set_error + //! — @c spawn cannot deliver an error to a non-existent caller. The + //! @c requires clause enforces this with a + //! __never_sends check; the diagnostic + //! overload says "spawn expects a sender that cannot fail" if the check + //! fires. + //! + //! Successful and stopped completions are both accepted; their results + //! are discarded. + //! + //! **Scope semantics.** + //! + //! The scope is the *owner of lifetime* for the spawned operation. + //! Calling code is expected to eventually @c join() the scope (or + //! otherwise wait for all spawned work to drain) before destroying it + //! — typically once at program shutdown, or once per logical unit of + //! related background work. + //! + //! @c spawn is the canonical fire-and-forget consumer for any work + //! that has a clear "owning context" (a request, a session, a worker). + //! For top-level work with no owning scope, use @c exec::start_detached + //! (an stdexec extension). For fire-and-forget work whose completion + //! you want to *observe* (without blocking), use + //! @c stdexec::spawn_future. + //! + //! @see stdexec::spawn_future — like @c spawn, but returns a sender that completes + //! when the spawned work completes + //! @see exec::start_detached — scope-less fire-and-forget (extension) + //! @see stdexec::sync_wait — top-level synchronous wait that returns the result struct spawn_t { private: @@ -139,12 +204,27 @@ namespace STDEXEC using _spawn_sndr_t = _spawn_sndr_impl_t<_wrapped_sender_t<_Sender, _Token>, _Env>; public: + //! @brief Spawn @c __sndr into the scope identified by @c __tkn, using + //! a default (empty) environment. + //! + //! Equivalent to spawn(__sndr, __tkn, env<>{}). + //! + //! @tparam _Sender A sender type with no @c set_error_t completions. + //! @tparam _Token A type satisfying @c stdexec::scope_token. + //! @param __sndr The sender to launch. + //! @param __tkn The scope token identifying the owning scope. template void operator()(_Sender&& __sndr, _Token __tkn) const { return (*this)(static_cast<_Sender&&>(__sndr), static_cast<_Token&&>(__tkn), env<>{}); } + //! @brief Diagnostic overload — selected when the sender's completion + //! signatures include @c set_error_t. Emits a @c static_assert + //! explaining that @c spawn expects a sender that cannot fail. + //! + //! Not normally called; the @c requires clause on the primary overload + //! steers compilation here on a constraint failure. template void operator()(_Sender&&, _Token, _Env&&) const { @@ -154,6 +234,26 @@ namespace STDEXEC "spawn expects a sender that cannot fail"); } + //! @brief Spawn @c __sndr into the scope identified by @c __tkn, using + //! the allocator queried from @c __env. + //! + //! Allocates the operation state on the heap (using + //! stdexec::get_allocator(__env), falling back to + //! @c std::allocator), associates with the scope via + //! __tkn.try_associate(), and on success @c start s the + //! operation. On completion the state is destroyed and deallocated. + //! + //! @tparam _Sender A sender type with no @c set_error_t completions. + //! @tparam _Token A type satisfying @c stdexec::scope_token. + //! @tparam _Env An environment type; queried for an allocator. + //! + //! @param __sndr The sender to launch. + //! @param __tkn The scope token identifying the owning scope. + //! @param __env Environment used both for allocator lookup and as + //! the spawned operation's receiver environment. + //! + //! @pre @c __sndr must not be able to complete with @c set_error + //! (enforced by the @c requires clause). template requires __never_sends, _Env> void operator()(_Sender&& __sndr, _Token __tkn, _Env&& __env) const @@ -192,6 +292,12 @@ namespace STDEXEC using __spawn::spawn_t; + //! @brief The customization point object for the @c spawn sender consumer. + //! + //! @c spawn is an instance of @ref spawn_t. See @ref spawn_t for the full + //! description, scope semantics, and a usage example. + //! + //! @hideinitializer inline constexpr spawn_t spawn{}; } // namespace STDEXEC diff --git a/include/stdexec/__detail/__spawn_future.hpp b/include/stdexec/__detail/__spawn_future.hpp index 2f65d8dcc..6f234c0f9 100644 --- a/include/stdexec/__detail/__spawn_future.hpp +++ b/include/stdexec/__detail/__spawn_future.hpp @@ -588,14 +588,105 @@ namespace STDEXEC } }; + //! @brief A sender consumer that eagerly starts a sender into an async + //! scope *and* returns a sender that completes when the spawned + //! work completes. + //! + //! @c spawn_future combines @ref spawn_t's "fire and forget into a + //! scope" semantics with an *observation channel*. Where @c spawn + //! returns @c void and discards the result of the spawned sender, + //! @c spawn_future returns a sender that, when connected and started, + //! delivers whatever completion the spawned operation produced — + //! value, error, or stopped. + //! + //! Like @c spawn, @c spawn_future eagerly starts the input sender at + //! the moment it is called. The returned sender is *not* a re-runnable + //! handle to that work; it is a one-shot observer of the already-running + //! operation. If the scope refuses to associate the operation (because + //! it has already begun shutting down, for example), the returned + //! sender completes via @c set_stopped without ever running the input + //! sender. + //! + //! See [exec.spawn.future] in the C++26 working draft for the + //! normative specification. + //! + //! @code{.cpp} + //! exec::async_scope scope; + //! + //! auto future = stdexec::spawn_future( + //! stdexec::just(42) | stdexec::then([](int x) { return x * 2; }), + //! scope.get_token()); + //! + //! // Do something else in parallel ... + //! + //! auto [v] = stdexec::sync_wait(std::move(future)).value(); + //! // v == 84; the spawned work was running while we did other things. + //! + //! stdexec::sync_wait(scope.join()); + //! @endcode + //! + //! **Eager vs. lazy.** + //! + //! Unlike most senders (which are *lazy* — they do nothing until + //! connected and started), the work that @c spawn_future observes is + //! *eager*: it starts at the call to @c spawn_future, not at @c start + //! of the returned sender. Connecting and starting the returned sender + //! is what you do to *observe* the result; it does not control when + //! the work runs. This makes @c spawn_future the natural way to fan + //! out concurrent work and later collect each result individually. + //! + //! **Why a scope?** + //! + //! As with @c spawn, the scope is the owner of lifetime for the + //! spawned operation. Without one, eager start would have no + //! defensible cleanup story at program shutdown. If you want to + //! observe a result and don't have a scope, you almost always want + //! @c sync_wait or a coroutine `co_await` over the original sender + //! instead — both are lazy. + //! + //! @see stdexec::spawn — like @c spawn_future but discards the result + //! @see exec::start_detached — scope-less fire-and-forget (extension) + //! @see stdexec::sync_wait — top-level synchronous wait that returns the result + //! @see stdexec::when_all — combine multiple senders concurrently (lazy) struct spawn_future_t { + //! @brief Spawn @c __sndr into the scope identified by @c __tkn, + //! eagerly start it, and return a sender that completes when + //! the spawned operation completes. + //! + //! Equivalent to spawn_future(__sndr, __tkn, env<>{}). + //! + //! @tparam _Sender A type satisfying @c stdexec::sender. + //! @tparam _Token A type satisfying @c stdexec::scope_token. + //! + //! @param __sndr The sender to launch. Eagerly started. + //! @param __tkn The scope token identifying the owning scope. + //! + //! @returns A sender that, when connected and started, completes with + //! the result of the eagerly-started @c __sndr (value, error, + //! or stopped), or with @c set_stopped if association with + //! the scope failed. template auto operator()(_Sender&& __sndr, _Token&& __tkn) const -> __well_formed_sender auto { return (*this)(static_cast<_Sender&&>(__sndr), static_cast<_Token&&>(__tkn), env<>{}); } + //! @brief Spawn @c __sndr into the scope identified by @c __tkn, + //! using the allocator queried from @c __env, and return a + //! sender that observes its completion. + //! + //! @tparam _Sender A type satisfying @c stdexec::sender. + //! @tparam _Token A type satisfying @c stdexec::scope_token. + //! @tparam _Env An environment type; queried for an allocator. + //! + //! @param __sndr The sender to launch. + //! @param __tkn The scope token identifying the owning scope. + //! @param __env Environment used both for allocator lookup and as + //! the spawned operation's receiver environment. + //! + //! @returns A sender observing the spawned operation's completion + //! (or @c set_stopped if scope-association failed). template auto operator()(_Sender&& __sndr, _Token&& __tkn, _Env&& __env) const -> __well_formed_sender auto @@ -776,8 +867,13 @@ namespace STDEXEC using __spawn_future::spawn_future_t; - /// @brief The spawn_future sender adaptor - /// @hideinitializer + //! @brief The customization point object for the @c spawn_future sender consumer. + //! + //! @c spawn_future is an instance of @ref spawn_future_t. See + //! @ref spawn_future_t for the full description, the eager-start + //! semantics, and a usage example. + //! + //! @hideinitializer inline constexpr spawn_future_t spawn_future{}; template <> diff --git a/include/stdexec/__detail/__starts_on.hpp b/include/stdexec/__detail/__starts_on.hpp index d9490df97..f37affa40 100644 --- a/include/stdexec/__detail/__starts_on.hpp +++ b/include/stdexec/__detail/__starts_on.hpp @@ -106,8 +106,104 @@ namespace STDEXEC ///////////////////////////////////////////////////////////////////////////// // [exec.starts.on] + + //! @brief A sender adaptor that runs a sender starting on the execution + //! resource associated with a given scheduler. + //! + //! @c starts_on takes a scheduler @c sched and a sender @c sndr and produces + //! a sender that, when connected and started, first hops onto @c sched's + //! execution resource and then runs @c sndr there. The completions of the + //! produced sender are delivered to the connected receiver from @c sched's + //! resource — there is no "round trip" back to whatever scheduler started + //! the operation (compare @ref on_t). + //! + //! Unlike most sender adaptors in stdexec, @c starts_on has no *pipe form*: + //! it is always called as starts_on(sched, sndr), never + //! sndr | starts_on(sched). This reflects the spec: @c starts_on + //! takes the scheduler *first*, mirroring the order of operations + //! (schedule, then run). + //! + //! @code{.cpp} + //! auto s = stdexec::starts_on(some_sched, sndr); + //! @endcode + //! + //! See [exec.starts.on] in the C++26 working draft for the normative + //! specification. + //! + //! **Equivalence.** + //! + //! Semantically, starts_on(sch, sndr) is equivalent to + //! + //! @code{.cpp} + //! schedule(sch) | let_value([sndr = std::forward(sndr)] { + //! return std::move(sndr); + //! }) + //! @endcode + //! + //! stdexec's implementation is structured differently for efficiency on + //! GPU contexts (it avoids making @c sndr dependent on the + //! schedule-completion), but the observable semantics match. + //! + //! **Completion signatures.** + //! + //! The resulting sender's completion signatures are essentially those of + //! @c sndr, augmented with any error completion that the scheduling step + //! itself can produce: + //! + //! @code{.cpp} + //! // (signatures of sndr) ... + //! set_error_t(/* scheduler error type, if scheduling may fail */) + //! set_stopped_t() // if not already present + //! @endcode + //! + //! If scheduling onto @c sch fails, an error completion is delivered to + //! the receiver on an *unspecified* execution agent. + //! + //! **Cancellation.** + //! + //! If the receiver requests stop before scheduling has produced its + //! value-completion, the resulting sender typically completes via + //! @c set_stopped rather than starting @c sndr at all — the precise + //! behavior depends on the scheduler. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto sched = get_parallel_scheduler(); + //! + //! // Run the entire chain on `sched`: + //! auto sndr = + //! starts_on(sched, just(21) | then([](int x) { return x * 2; })); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! (void)v; // == 42, computed on `sched` + //! } + //! @endcode + //! + //! @see stdexec::schedule — the primitive that produces a schedule-sender + //! @see stdexec::continues_on — transfer to a scheduler *after* a sender completes + //! @see stdexec::on — run on a scheduler then transfer back to the original struct starts_on_t { + //! @brief Construct a sender that runs @c __sndr on @c __sched's + //! execution resource. + //! + //! @tparam _Scheduler A type satisfying the @c stdexec::scheduler concept. + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! + //! @param __sched The scheduler whose execution resource will host + //! @c __sndr. + //! @param __sndr The sender to run on @c __sched. + //! + //! @returns A sender that, when connected to a receiver and started, + //! first schedules onto @c __sched, then connects and starts + //! @c __sndr there, and forwards @c __sndr's completion to the + //! receiver. template constexpr auto operator()(_Scheduler&& __sched, _Sender&& __sndr) const -> __well_formed_sender auto @@ -136,6 +232,12 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c starts_on sender adaptor. + //! + //! @c starts_on is an instance of @ref starts_on_t. See @ref starts_on_t + //! for the full description, completion signatures, and a usage example. + //! + //! @hideinitializer inline constexpr starts_on_t starts_on{}; template <> diff --git a/include/stdexec/__detail/__stopped_as_error.hpp b/include/stdexec/__detail/__stopped_as_error.hpp index b59b833b6..80212ace9 100644 --- a/include/stdexec/__detail/__stopped_as_error.hpp +++ b/include/stdexec/__detail/__stopped_as_error.hpp @@ -30,8 +30,87 @@ namespace STDEXEC { ///////////////////////////////////////////////////////////////////////////// // [execution.senders.adaptors.stopped_as_error] + + //! @brief A pipeable sender adaptor that converts a predecessor's stopped + //! completion into an error completion carrying a user-supplied + //! error value. + //! + //! @c stopped_as_error is a "translator" adaptor: it doesn't change what + //! happens in the value channel, but it rewrites a @c set_stopped + //! completion into a @c set_error completion with a specific datum. + //! This is useful when downstream code needs to *distinguish* a + //! cancellation from a real error and you want the cancellation to be + //! delivered as an error of a particular type (typically because the + //! consumer can't or won't handle the stopped channel). + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::stopped_as_error(sndr, my_error); + //! auto s2 = sndr | stdexec::stopped_as_error(my_error); + //! @endcode + //! + //! **Equivalence.** + //! + //! stopped_as_error(sndr, err) is implemented (and is + //! observationally equivalent to) + //! let_stopped(sndr, [err]{ return just_error(err); }). + //! Use this adaptor whenever you would have written that pattern by + //! hand — it is shorter, clearer at the call site, and the + //! implementation can be specialized more efficiently in the future. + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged + //! set_error_t(Es)... // forwarded unchanged + //! set_stopped_t() // consumed + //! @endcode + //! + //! the sender produced by stopped_as_error(sndr, err) has + //! completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged + //! set_error_t(Es)... // forwarded unchanged + //! set_error_t(std::decay_t) // the supplied error, decay-copied + //! @endcode + //! + //! The original @c set_stopped_t completion is replaced; the resulting + //! sender will never deliver @c set_stopped. + //! + //! **Example.** + //! + //! @code{.cpp} + //! using namespace stdexec; + //! + //! auto sndr = just_stopped() + //! | stopped_as_error(std::runtime_error{"cancelled"}); + //! + //! try { + //! sync_wait(std::move(sndr)); // throws std::runtime_error + //! } catch (std::runtime_error const& e) { + //! // e.what() == "cancelled" + //! } + //! @endcode + //! + //! @see stdexec::stopped_as_optional — convert stopped into a value-channel @c std::nullopt + //! @see stdexec::upon_stopped — handle stopped synchronously + //! @see stdexec::let_stopped — handle stopped with a sender-returning callback struct stopped_as_error_t { + //! @brief Construct a sender that translates @c __sndr's @c set_stopped + //! completion into a @c set_error completion carrying @c __err. + //! + //! @tparam _Sender A type satisfying @c stdexec::sender. + //! @tparam _Error A decayed, move-constructible error datum type + //! (satisfying the internal __movable_value concept). + //! + //! @param __sndr The predecessor sender. Forwarded into the result. + //! @param __err The error datum to deliver if @c __sndr is stopped. + //! Decay-copied into the resulting sender. template constexpr auto operator()(_Sender&& __sndr, _Error __err) const -> __well_formed_sender auto { @@ -41,6 +120,10 @@ namespace STDEXEC { return just_error(static_cast<_Error&&>(__err2)); }); } + //! @brief Construct a sender-adaptor closure for the pipe form. + //! + //! sndr | stopped_as_error(__err) is equivalent to + //! stopped_as_error(sndr, __err). template <__movable_value _Error> STDEXEC_ATTRIBUTE(always_inline) constexpr auto operator()(_Error __err) const noexcept(__nothrow_move_constructible<_Error>) @@ -49,6 +132,12 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c stopped_as_error sender adaptor. + //! + //! @c stopped_as_error is an instance of @ref stopped_as_error_t. See + //! @ref stopped_as_error_t for the full description and a usage example. + //! + //! @hideinitializer inline constexpr stopped_as_error_t stopped_as_error{}; } // namespace STDEXEC diff --git a/include/stdexec/__detail/__stopped_as_optional.hpp b/include/stdexec/__detail/__stopped_as_optional.hpp index b2b8a8d7b..68ce46ec7 100644 --- a/include/stdexec/__detail/__stopped_as_optional.hpp +++ b/include/stdexec/__detail/__stopped_as_optional.hpp @@ -136,14 +136,115 @@ namespace STDEXEC }; } // namespace __sao + //! @brief A pipeable sender adaptor that converts a predecessor's stopped + //! completion into a value-channel @c std::nullopt, wrapping the + //! value-completion datum in a @c std::optional. + //! + //! @c stopped_as_optional is the value-channel mirror of + //! @ref stopped_as_error_t. Where @c stopped_as_error turns cancellation + //! into an error, @c stopped_as_optional turns cancellation into a + //! "no value" signal *on the value channel*. The resulting sender + //! value-completes with a @c std::optional: engaged if the + //! predecessor produced a value, disengaged if the predecessor was + //! stopped. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::stopped_as_optional(sndr); + //! auto s2 = sndr | stdexec::stopped_as_optional(); + //! @endcode + //! + //! Use this when downstream code prefers branching on a @c std::optional + //! (a familiar idiom) over branching on an empty + //! std::optional> from @c sync_wait or + //! handling the @c set_stopped channel via an adaptor. + //! + //! **Single value-completion requirement.** + //! + //! @c stopped_as_optional requires the predecessor to have exactly one + //! value-completion signature with exactly one argument. (How would we + //! wrap multiple values in a *single* @c std::optional?) If the + //! predecessor has multiple value completions, or zero/multiple value + //! arguments, the program is ill-formed with a focused diagnostic + //! ("the sender must have exactly one value completion with one + //! argument"). + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(T) // exactly one value-completion with one argument + //! set_error_t(Es)... // zero or more + //! set_stopped_t() // consumed + //! @endcode + //! + //! the sender produced by stopped_as_optional(sndr) has + //! completion signatures + //! + //! @code{.cpp} + //! set_value_t(std::optional>) + //! set_error_t(Es)... // forwarded unchanged + //! set_error_t(std::exception_ptr) // added if wrapping may throw + //! // (no set_stopped_t in the output) + //! @endcode + //! + //! The original @c set_stopped_t completion is consumed: the resulting + //! sender will never deliver @c set_stopped. + //! + //! **Exception behavior.** + //! + //! If constructing the @c std::optional from the predecessor's value + //! throws (e.g., the value type's copy constructor throws), the + //! exception is delivered through + //! @c set_error_t(std::exception_ptr). + //! + //! **Example.** + //! + //! @code{.cpp} + //! using namespace stdexec; + //! + //! auto sndr = just(42) | stopped_as_optional(); + //! auto [opt] = sync_wait(std::move(sndr)).value(); + //! // opt == std::optional{42} + //! + //! auto sndr2 = just_stopped() | stopped_as_optional(); + //! // ...but to make stopped_as_optional well-formed here we need to give + //! // the predecessor a value-shape; in practice you compose it on a + //! // sender that may either succeed or be stopped: + //! auto pipeline = + //! /* some sender that produces an int or is stopped */ + //! | stopped_as_optional(); + //! @endcode + //! + //! @see stdexec::stopped_as_error — convert stopped into an error + //! @see stdexec::upon_stopped — handle stopped synchronously + //! @see stdexec::let_stopped — handle stopped with a sender-returning callback + //! @see stdexec::sync_wait — also uses an outer @c std::optional to signal stop struct stopped_as_optional_t { + //! @brief Construct a sender that wraps @c __sndr's value completion in + //! a @c std::optional and reroutes @c set_stopped to a disengaged + //! optional on the value channel. + //! + //! @tparam _Sender A type satisfying @c stdexec::sender whose + //! completion signatures include exactly one + //! @c set_value_t(T) signature. + //! + //! @param __sndr The predecessor sender. Forwarded into the result. template constexpr auto operator()(_Sender&& __sndr) const -> __well_formed_sender auto { return __make_sexpr(__(), static_cast<_Sender&&>(__sndr)); } + //! @brief Construct a sender-adaptor closure for the pipe form. + //! + //! sndr | stopped_as_optional() is equivalent to + //! stopped_as_optional(sndr). The empty parentheses are + //! required by the pipe-closure convention; there are no captured + //! arguments. STDEXEC_ATTRIBUTE(always_inline) auto operator()() const noexcept { @@ -151,6 +252,13 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c stopped_as_optional sender adaptor. + //! + //! @c stopped_as_optional is an instance of @ref stopped_as_optional_t. + //! See @ref stopped_as_optional_t for the full description and a usage + //! example. + //! + //! @hideinitializer inline constexpr stopped_as_optional_t stopped_as_optional{}; template <> diff --git a/include/stdexec/__detail/__sync_wait.hpp b/include/stdexec/__detail/__sync_wait.hpp index 308c0ab26..bb28eab1b 100644 --- a/include/stdexec/__detail/__sync_wait.hpp +++ b/include/stdexec/__detail/__sync_wait.hpp @@ -193,8 +193,93 @@ namespace STDEXEC::__sync_wait STDEXEC_P2300_NAMESPACE_BEGIN(this_thread) //////////////////////////////////////////////////////////////////////////// // [execution.senders.consumers.sync_wait] + + //! @brief A sender consumer that synchronously blocks the calling thread + //! until a sender completes and returns its result. + //! + //! @c sync_wait is the bridge from the asynchronous sender world back into + //! synchronous code. You give it a sender; it connects the sender to a + //! built-in receiver, starts the resulting operation, then drives an + //! internal @c run_loop on the calling thread until the operation + //! completes. The result is returned as a std::optional of a + //! tuple of the value-completion datums. + //! + //! This is the most common way to "run" a sender in a top-level program or + //! a test — it's what you reach for in a @c main() or when synchronously + //! waiting on a single sub-pipeline. For fire-and-forget execution, prefer + //! @c exec::start_detached or @c stdexec::spawn. + //! + //! @code{.cpp} + //! auto [v] = stdexec::sync_wait(stdexec::just(42)).value(); + //! // v == 42 + //! @endcode + //! + //! See [exec.sync.wait] in the C++26 working draft for the normative + //! specification. + //! + //! **Completion behavior.** + //! + //! Given an input sender @c sndr that, in some environment, completes with + //! exactly one of: + //! + //! | Sender completion | What @c sync_wait does | + //! | ----------------------------- | --------------------------------------------------------------- | + //! | @c set_value_t(Vs...) | Returns @c std::optional> engaged. | + //! | @c set_error_t(std::exception_ptr) | Rethrows the exception via @c std::rethrow_exception. | + //! | @c set_error_t(std::error_code) | Throws @c std::system_error(error_code). | + //! | @c set_error_t(E) | Throws @c E directly. | + //! | @c set_stopped_t() | Returns an empty (disengaged) @c std::optional. | + //! + //! **Single-value-completion requirement.** + //! + //! @c sync_wait *mandates* that its argument sender have exactly one + //! @c set_value_t completion signature. A sender that can succeed in more + //! than one way (e.g. just(1) | when_all(just(std::string{"x"})) + //! yielding two distinct tuples) requires @c sync_wait_with_variant + //! instead. The static assertion in @c sync_wait will point this out at + //! compile time, with a hint to use the variant form. + //! + //! **Delegation scheduler.** + //! + //! The internal @c run_loop is exposed via @c get_delegation_scheduler on + //! the receiver's environment, so senders that need to enqueue work back + //! onto the waiting thread (e.g. continuations after an I/O wait) can do + //! so safely. This is what enables algorithms like @c continues_on to + //! return execution to the calling thread of @c sync_wait. + //! + //! **When *not* to use** @c sync_wait **:** + //! - On any thread that participates in an event loop or executor — you + //! will block it. @c sync_wait is for top-level synchronization + //! (main, tests, leaf utilities), not pipeline composition. + //! - When you don't need the result. Use @c exec::start_detached or + //! @c stdexec::spawn for fire-and-forget. + //! + //! @see stdexec::sync_wait_with_variant — sync_wait for multi-completion senders + //! @see exec::start_detached — fire-and-forget consumer (no result) + //! @see stdexec::spawn — fire-and-forget into a scope + //! @see stdexec::spawn_future — spawn into a scope and observe via a sender struct sync_wait_t { + //! @brief Connect @c __sndr to an internal receiver, start the operation, + //! and drive a @c run_loop until completion. + //! + //! @tparam _CvSender A type satisfying @c stdexec::sender_in for the + //! built-in @c sync_wait environment. + //! @param __sndr The sender to drive to completion. Must have + //! exactly one @c set_value_t completion signature. + //! + //! @returns @c std::optional> where @c Vs... are the + //! value-completion datum types of @c __sndr. The optional is + //! engaged on @c set_value, disengaged on @c set_stopped. + //! + //! @throws The error datum, if @c __sndr completes with @c set_error + //! (rethrown via @c std::rethrow_exception for + //! @c std::exception_ptr, via @c std::system_error for + //! @c std::error_code, or directly otherwise). + //! + //! @pre @c __sndr must have exactly one @c set_value_t completion + //! signature, otherwise the program is ill-formed with a + //! diagnostic pointing at @c sync_wait_with_variant. template _CvSender> auto operator()(_CvSender&& __sndr) const { @@ -274,30 +359,10 @@ STDEXEC_P2300_NAMESPACE_BEGIN(this_thread) return std::optional>{}; } - /// @brief Synchronously wait for the result of a sender, blocking the - /// current thread. - /// - /// `sync_wait` connects and starts the given sender, and then drives a - /// `run_loop` instance until the sender completes. Additional work - /// can be delegated to the `run_loop` by scheduling work on the - /// scheduler returned by calling `get_delegation_scheduler` on the - /// receiver's environment. - /// - /// @pre The sender must have a exactly one value completion signature. That - /// is, it can only complete successfully in one way, with a single - /// set of values. - /// - /// @retval success Returns an engaged `std::optional` containing the result - /// values in a `std::tuple`. - /// @retval canceled Returns an empty `std::optional`. - /// @retval error Throws the error. - /// - /// @throws std::rethrow_exception(error) if the error has type - /// `std::exception_ptr`. - /// @throws std::system_error(error) if the error has type - /// `std::error_code`. - /// @throws error otherwise - + //! @internal + //! @brief Default-domain implementation of @c sync_wait. Connects + //! @c __sndr, starts the operation, drives an internal @c run_loop, and + //! returns/throws per @ref sync_wait_t. Not normally called by users. template _CvSender> STDEXEC_CONSTEXPR_CXX23 auto apply_sender(_CvSender&& __sndr) const // -> std::optional> @@ -327,8 +392,67 @@ STDEXEC_P2300_NAMESPACE_BEGIN(this_thread) //////////////////////////////////////////////////////////////////////////// // [execution.senders.consumers.sync_wait_with_variant] + + //! @brief A sender consumer that synchronously blocks the calling thread + //! until a multi-value-completion sender completes, returning the + //! result as a variant of tuples. + //! + //! @c sync_wait_with_variant is the multi-completion sibling of + //! @ref sync_wait_t. A sender that can succeed in more than one way — for + //! example, an algorithm that may complete with either an @c int or a + //! @c std::string — cannot be passed to @c sync_wait, because the latter + //! returns a single fixed tuple type. @c sync_wait_with_variant accepts + //! such senders and returns the result as a @c std::variant of all the + //! possible value-tuple shapes. + //! + //! @code{.cpp} + //! // sndr completes with either set_value_t(int) or set_value_t(std::string). + //! auto opt = stdexec::sync_wait_with_variant(std::move(sndr)); + //! if (opt) { + //! std::visit([](auto&& tup) { + //! // tup is either std::tuple or std::tuple. + //! }, *opt); + //! } + //! @endcode + //! + //! See [exec.sync.wait.var] in the C++26 working draft for the normative + //! specification. + //! + //! **Completion behavior.** + //! + //! Given an input sender @c sndr with value-completion signatures + //! set_value_t(Vs1...), set_value_t(Vs2...), ..., the return type is + //! + //! @code{.cpp} + //! std::optional, std::tuple, ...>> + //! @endcode + //! + //! The handling of @c set_error_t and @c set_stopped_t matches + //! @ref sync_wait_t: errors are thrown, @c set_stopped yields a disengaged + //! optional. + //! + //! **When to use** @c sync_wait_with_variant **vs.** @c sync_wait **:** + //! Use @c sync_wait when the sender has *exactly one* value-completion + //! shape; use @c sync_wait_with_variant otherwise. @c sync_wait's static + //! assertion will steer you here if needed. + //! + //! @see stdexec::sync_wait — for single-value-completion senders + //! @see stdexec::into_variant — adaptor that collapses multi-completion senders into a variant struct sync_wait_with_variant_t { + //! @brief Connect @c __sndr, start the operation, drive a @c run_loop + //! until completion, and return the result as a variant of tuples. + //! + //! @tparam _CvSender A type satisfying @c stdexec::sender_in for the + //! @c sync_wait environment. + //! @param __sndr The sender to drive to completion. May have any + //! number of @c set_value_t completion signatures. + //! + //! @returns @c std::optional, ...>> + //! engaged on @c set_value, disengaged on @c set_stopped. + //! + //! @throws The error datum, if @c __sndr completes with @c set_error, + //! using the same rules as @ref sync_wait_t. template _CvSender> requires STDEXEC::__callablestd::invoke to the result of a synchronous computation, and + //! is the most common sender adaptor in practice; most sender pipelines contain at + //! least one @c then. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::then(sndr, f); // direct invocation + //! auto s2 = sndr | stdexec::then(f); // pipe syntax + //! @endcode + //! + //! The two forms are expression-equivalent. See [exec.then] in the + //! C++26 working draft for the normative specification. + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // one or more value completions + //! set_error_t(Es)... // zero or more error completions + //! set_stopped_t() // optional stopped completion + //! @endcode + //! + //! the sender produced by then(sndr, f) has completion signatures + //! + //! @code{.cpp} + //! set_value_t(R) // R = decltype(std::invoke(f, Vs...)) + //! // (or set_value_t() when R is void) + //! set_error_t(Es)... // forwarded unchanged from sndr + //! set_error_t(std::exception_ptr) // added when invoking f may throw + //! set_stopped_t() // forwarded unchanged from sndr + //! @endcode + //! + //! If @c sndr has multiple value completions, @c f must be invocable with every + //! one of them; otherwise the program is ill-formed and the diagnostic surfaces + //! at the point where the resulting sender is connected to a receiver. The + //! resulting value-completion arity is always one: each distinct return type + //! @c R from invoking @c f contributes a @c set_value_t(R) overload. + //! + //! **Exception behavior.** + //! + //! If invoking @c f throws, the exception is delivered through + //! @c set_error_t(std::exception_ptr) on the resulting sender. When @c f is + //! @c noexcept for every value-argument pack of @c sndr, no additional + //! @c std::exception_ptr error completion is added. + //! + //! **Cancellation.** + //! + //! @c then does not interact with the receiver's stop token. When @c sndr + //! completes with @c set_stopped, @c f is not invoked and the stopped + //! completion is forwarded to the downstream receiver. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto sndr = just(21) + //! | then([](int x) { return x * 2; }) + //! | then([](int x) { return x + 1; }); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == 43); + //! } + //! @endcode + //! + //! @see stdexec::upon_error — adapt the error channel + //! @see stdexec::upon_stopped — adapt the stopped channel + //! @see stdexec::let_value — adapt the value channel with a sender-returning function struct then_t { + //! @brief Construct a sender that adapts @c __sndr by invoking @c __fun with + //! each value-completion argument pack it produces. + //! + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! @tparam _Fun A decayed, move-constructible callable type + //! (satisfying the internal __movable_value concept). + //! + //! @param __sndr The predecessor sender whose value-completion is to be + //! adapted. Perfect-forwarded into the resulting sender, so an + //! rvalue is moved and an lvalue copied as needed. + //! @param __fun The function (or callable) to invoke with each + //! value-completion of @c __sndr. Stored by value (decayed) in + //! the resulting sender. + //! + //! @returns A sender that, when connected to a receiver and started, drives + //! @c __sndr and routes each of its value-completions through + //! @c __fun. The error and stopped channels are forwarded unchanged. + //! + //! @pre @c __fun must be invocable with every value-completion argument pack of + //! @c __sndr (with appropriate value categories). Otherwise the program + //! is ill-formed at the point where the resulting sender is connected to + //! a receiver. template constexpr auto operator()(_Sender&& __sndr, _Fun __fun) const -> __well_formed_sender auto { return __make_sexpr(static_cast<_Fun&&>(__fun), static_cast<_Sender&&>(__sndr)); } + //! @brief Construct a sender-adaptor closure that, when applied to a sender, + //! produces then(sndr, __fun). + //! + //! This overload enables the pipe syntax: sndr | then(__fun) is + //! equivalent to then(sndr, __fun). + //! + //! @tparam _Fun A decayed, move-constructible callable type + //! (satisfying the internal __movable_value concept). + //! @param __fun The callable to invoke on the predecessor's value completions + //! when the closure is later applied to a sender. + //! + //! @returns A sender-adaptor closure object that captures @c __fun by value. + //! When piped against a sender @c sndr, it yields the sender + //! then(sndr, std::move(__fun)). template <__movable_value _Fun> STDEXEC_ATTRIBUTE(always_inline) constexpr auto operator()(_Fun __fun) const @@ -101,8 +214,12 @@ namespace STDEXEC } }; - //! @brief The \c then sender adaptor, which invokes a function with the result of - //! a sender, making the result available to the next receiver. + //! @brief The customization point object for the @c then sender adaptor. + //! + //! @c then is an instance of @ref then_t. See @ref then_t for the full + //! description, the completion-signature transformation rules, exception and + //! cancellation behavior, and a usage example. + //! //! @hideinitializer inline constexpr then_t then{}; diff --git a/include/stdexec/__detail/__upon_error.hpp b/include/stdexec/__detail/__upon_error.hpp index 957cb688d..e418f11a7 100644 --- a/include/stdexec/__detail/__upon_error.hpp +++ b/include/stdexec/__detail/__upon_error.hpp @@ -74,14 +74,130 @@ namespace STDEXEC } // namespace __upon_error //////////////////////////////////////////////////////////////////////////////////////////////// + //! @brief A pipeable sender adaptor that handles a predecessor sender's + //! error completion by invoking a callable on the error datum. + //! + //! @c upon_error maps the error channel of a sender through a function while + //! forwarding the value and stopped channels unchanged. The function's return + //! value becomes a *value* completion on the resulting sender — so + //! @c upon_error is the canonical way to *recover from* an error: turn it + //! into a substitute value and continue the pipeline as if nothing had gone + //! wrong. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::upon_error(sndr, f); // direct invocation + //! auto s2 = sndr | stdexec::upon_error(f); // pipe syntax + //! @endcode + //! + //! The two forms are expression-equivalent. See [exec.then] in the + //! C++26 working draft for the normative specification (@c upon_error is + //! specified alongside @c then and @c upon_stopped). + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged + //! set_error_t(Es)... // one or more error completions + //! set_stopped_t() // forwarded unchanged (if present) + //! @endcode + //! + //! the sender produced by upon_error(sndr, f) has completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged from sndr + //! set_value_t(R) // R = decltype(std::invoke(f, E)) for each E + //! // (or set_value_t() when R is void) + //! set_error_t(std::exception_ptr) // added when invoking f may throw + //! set_stopped_t() // forwarded unchanged from sndr + //! @endcode + //! + //! For each distinct error type @c E that @c sndr may complete with, @c f + //! must be invocable with @c E (with appropriate value category). Otherwise + //! the program is ill-formed at the point where the resulting sender is + //! connected to a receiver. All original @c set_error_t completions are + //! *replaced* by the union of value completions produced by @c f — only + //! errors thrown by @c f itself remain on the error channel. + //! + //! **Exception behavior.** + //! + //! If invoking @c f throws, the exception is delivered through + //! @c set_error_t(std::exception_ptr) on the resulting sender. When @c f is + //! @c noexcept for every error type of @c sndr, no @c std::exception_ptr + //! error completion is added. + //! + //! **Cancellation.** + //! + //! @c upon_error does not interact with the receiver's stop token. When + //! @c sndr completes with @c set_stopped, @c f is not invoked and the + //! stopped completion is forwarded to the downstream receiver. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto sndr = just_error(std::error_code{ENOENT, std::system_category()}) + //! | upon_error([](std::error_code) { return -1; }); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == -1); + //! } + //! @endcode + //! + //! @see stdexec::then — adapt the value channel + //! @see stdexec::upon_stopped — adapt the stopped channel + //! @see stdexec::let_error — adapt the error channel with a sender-returning function struct upon_error_t { + //! @brief Construct a sender that handles each error completion of @c __sndr + //! by invoking @c __fun on the error datum. + //! + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! @tparam _Fun A decayed, move-constructible callable type + //! (satisfying the internal __movable_value concept). + //! + //! @param __sndr The predecessor sender whose error-completions are to be + //! adapted. Perfect-forwarded into the resulting sender. + //! @param __fun The function (or callable) to invoke with each + //! error-completion datum of @c __sndr. Stored by value + //! (decayed) in the resulting sender. + //! + //! @returns A sender that, when connected to a receiver and started, drives + //! @c __sndr and routes each of its error-completions through + //! @c __fun, delivering the result on the value channel. The + //! value and stopped channels of @c __sndr are forwarded unchanged. + //! + //! @pre @c __fun must be invocable with every error type of @c __sndr + //! (with appropriate value categories). Otherwise the program is + //! ill-formed at the point where the resulting sender is connected + //! to a receiver. template constexpr auto operator()(_Sender&& __sndr, _Fun __fun) const -> __well_formed_sender auto { return __make_sexpr(static_cast<_Fun&&>(__fun), static_cast<_Sender&&>(__sndr)); } + //! @brief Construct a sender-adaptor closure that, when applied to a sender, + //! produces upon_error(sndr, __fun). + //! + //! This overload enables the pipe syntax: sndr | upon_error(__fun) + //! is equivalent to upon_error(sndr, __fun). + //! + //! @tparam _Fun A decayed, move-constructible callable type. + //! @param __fun The callable to invoke on the predecessor's error + //! completions when the closure is later applied to a sender. + //! + //! @returns A sender-adaptor closure object that captures @c __fun by value. + //! When piped against a sender @c sndr, it yields the sender + //! upon_error(sndr, std::move(__fun)). template <__movable_value _Fun> STDEXEC_ATTRIBUTE(always_inline) constexpr auto operator()(_Fun __fun) const noexcept(__nothrow_move_constructible<_Fun>) @@ -90,6 +206,13 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c upon_error sender adaptor. + //! + //! @c upon_error is an instance of @ref upon_error_t. See @ref upon_error_t + //! for the full description, completion-signature transformation rules, + //! exception and cancellation behavior, and a usage example. + //! + //! @hideinitializer inline constexpr upon_error_t upon_error{}; template <> diff --git a/include/stdexec/__detail/__upon_stopped.hpp b/include/stdexec/__detail/__upon_stopped.hpp index 910db47cc..34b3ebfc8 100644 --- a/include/stdexec/__detail/__upon_stopped.hpp +++ b/include/stdexec/__detail/__upon_stopped.hpp @@ -75,8 +75,106 @@ namespace STDEXEC } // namespace __upon_stopped //////////////////////////////////////////////////////////////////////////////////////////////// + //! @brief A pipeable sender adaptor that handles a predecessor sender's + //! stopped completion by invoking a nullary callable. + //! + //! @c upon_stopped maps the stopped channel of a sender into a value + //! completion by invoking a callable with *no arguments* and forwarding its + //! return value downstream. The value and error channels are forwarded + //! unchanged. This is the canonical way to *recover from* cancellation: + //! turn a stopped completion into a substitute value and continue. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::upon_stopped(sndr, f); // direct invocation + //! auto s2 = sndr | stdexec::upon_stopped(f); // pipe syntax + //! @endcode + //! + //! The two forms are expression-equivalent. See [exec.then] in the + //! C++26 working draft for the normative specification (@c upon_stopped is + //! specified alongside @c then and @c upon_error). + //! + //! **Completion signatures.** + //! + //! Given a predecessor sender @c sndr with completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged + //! set_error_t(Es)... // forwarded unchanged + //! set_stopped_t() // (must be present) + //! @endcode + //! + //! the sender produced by upon_stopped(sndr, f) has completion signatures + //! + //! @code{.cpp} + //! set_value_t(Vs...) // forwarded unchanged from sndr + //! set_value_t(R) // R = decltype(std::invoke(f)) + //! // (or set_value_t() when R is void) + //! set_error_t(Es)... // forwarded unchanged from sndr + //! set_error_t(std::exception_ptr) // added when invoking f may throw + //! // (no set_stopped_t in the output) + //! @endcode + //! + //! @c f must be invocable with *no* arguments — this requirement is enforced + //! by the @c requires clause on the operator overloads. The original + //! @c set_stopped_t completion is *consumed*: the resulting sender will + //! never complete via @c set_stopped (unless @c f itself returns a sender + //! that does, which @c upon_stopped does not — see @c let_stopped for that). + //! + //! **Exception behavior.** + //! + //! If invoking @c f throws, the exception is delivered through + //! @c set_error_t(std::exception_ptr) on the resulting sender. When @c f is + //! @c noexcept, no @c std::exception_ptr error completion is added. + //! + //! **Cancellation.** + //! + //! @c upon_stopped does not interact with the receiver's stop token. It + //! reacts to the predecessor's @c set_stopped by invoking @c f; it does + //! not itself initiate cancellation. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! + //! auto sndr = just_stopped() + //! | upon_stopped([] { return 42; }); + //! + //! auto [v] = sync_wait(std::move(sndr)).value(); + //! assert(v == 42); + //! } + //! @endcode + //! + //! @see stdexec::then — adapt the value channel + //! @see stdexec::upon_error — adapt the error channel + //! @see stdexec::let_stopped — adapt the stopped channel with a sender-returning function struct upon_stopped_t { + //! @brief Construct a sender that handles a stopped completion of @c __sndr + //! by invoking @c __fun and delivering its return value. + //! + //! @tparam _Sender A type satisfying the @c stdexec::sender concept. + //! @tparam _Fun A decayed, move-constructible, *nullary* callable type. + //! + //! @param __sndr The predecessor sender whose stopped completion is to be + //! adapted. Perfect-forwarded into the resulting sender. + //! @param __fun The function (or callable) to invoke when @c __sndr + //! completes via @c set_stopped. Stored by value + //! (decayed) in the resulting sender. + //! + //! @returns A sender that, when connected to a receiver and started, drives + //! @c __sndr and reacts to its @c set_stopped completion by + //! invoking @c __fun and forwarding the result via @c set_value. + //! The value and error channels of @c __sndr are forwarded unchanged. + //! + //! @pre @c __fun must be invocable with no arguments (the @c requires + //! clause enforces this). Otherwise the call is not viable. template requires __callable<_Fun> auto operator()(_Sender&& __sndr, _Fun __fun) const -> __well_formed_sender auto @@ -85,6 +183,19 @@ namespace STDEXEC static_cast<_Sender&&>(__sndr)); } + //! @brief Construct a sender-adaptor closure that, when applied to a sender, + //! produces upon_stopped(sndr, __fun). + //! + //! This overload enables the pipe syntax: sndr | upon_stopped(__fun) + //! is equivalent to upon_stopped(sndr, __fun). + //! + //! @tparam _Fun A decayed, move-constructible, *nullary* callable type. + //! @param __fun The callable to invoke on the predecessor's stopped + //! completion when the closure is later applied to a sender. + //! + //! @returns A sender-adaptor closure object that captures @c __fun by value. + //! When piped against a sender @c sndr, it yields the sender + //! upon_stopped(sndr, std::move(__fun)). template <__movable_value _Fun> requires __callable<_Fun> STDEXEC_ATTRIBUTE(always_inline) @@ -94,6 +205,14 @@ namespace STDEXEC } }; + //! @brief The customization point object for the @c upon_stopped sender adaptor. + //! + //! @c upon_stopped is an instance of @ref upon_stopped_t. See + //! @ref upon_stopped_t for the full description, completion-signature + //! transformation rules, exception and cancellation behavior, and a usage + //! example. + //! + //! @hideinitializer inline constexpr upon_stopped_t upon_stopped{}; template <> diff --git a/include/stdexec/__detail/__when_all.hpp b/include/stdexec/__detail/__when_all.hpp index 4e18a3c31..827aace51 100644 --- a/include/stdexec/__detail/__when_all.hpp +++ b/include/stdexec/__detail/__when_all.hpp @@ -47,8 +47,132 @@ namespace STDEXEC ///////////////////////////////////////////////////////////////////////////// // [execution.senders.adaptors.when_all] // [execution.senders.adaptors.when_all_with_variant] + //! @brief A variadic sender factory that runs multiple senders concurrently + //! and completes when all of them have completed, concatenating + //! their value datums. + //! + //! @c when_all is the canonical *parallel composition* primitive in the + //! sender model. You give it one or more senders; it returns a single + //! sender that, when connected and started, starts *all* of the input + //! senders concurrently. When every input has completed, @c when_all's + //! sender completes with a value tuple that is the concatenation of every + //! input's value datums. + //! + //! If any one input fails or is stopped, @c when_all requests stop on the + //! others (via an internal @c inplace_stop_source) and completes with + //! that error (or with @c set_stopped). This makes @c when_all naturally + //! fail-fast: as soon as one branch has gone bad, the rest are asked to + //! wind down. + //! + //! @code{.cpp} + //! auto s = stdexec::when_all( + //! stdexec::just(1), + //! stdexec::just(2.5), + //! stdexec::just(std::string{"x"})); + //! auto [i, d, str] = stdexec::sync_wait(std::move(s)).value(); + //! // i == 1, d == 2.5, str == "x" + //! @endcode + //! + //! See [exec.when.all] in the C++26 working draft for the normative + //! specification. + //! + //! **Single value-completion requirement.** + //! + //! @c when_all requires that each input sender have exactly one + //! @c set_value_t completion signature — otherwise the *output* value + //! signature would be a combinatorial explosion of all possible + //! concatenations. The constraint is enforced at connect time with a + //! diagnostic pointing at @ref when_all_with_variant_t, which lifts the + //! restriction (at the cost of producing a variant per input). + //! + //! **Completion signatures.** + //! + //! Given inputs @c sndr_i with completion signatures + //! + //! @code{.cpp} + //! // For each i in 1..n: + //! set_value_t(Vi...) // exactly one such signature per input + //! set_error_t(Eij)... // zero or more per input + //! set_stopped_t() // optional per input + //! @endcode + //! + //! the resulting sender has completion signatures: + //! + //! @code{.cpp} + //! set_value_t(V1..., V2..., ..., Vn...) // concatenation of every input + //! set_error_t(Eij)... // union across all inputs + //! set_error_t(std::exception_ptr) // added if any decay-copy may throw + //! set_stopped_t() // added if any input has it, + //! // or if cancellation may happen + //! @endcode + //! + //! The value datums of each input are decay-copied into the resulting + //! sender's state while it waits for the slowest input to finish; the + //! final tuple is built from those decay-copies. If any decay-copy + //! throws, the operation transitions to the error path. + //! + //! **Concurrency.** + //! + //! "Concurrently" here means *the inputs are not sequenced relative to + //! each other* — @c when_all starts every child operation in a fold + //! expression before returning from @c start. Whether they actually + //! execute in parallel depends on the schedulers they're attached to: + //! independent @c starts_on / @c continues_on branches across different + //! schedulers truly run in parallel; multiple @c just-rooted branches + //! all complete synchronously inside @c when_all's @c start. + //! + //! **Error and stop semantics.** + //! + //! At most *one* completion is delivered to the downstream receiver. If + //! several children produce errors or stopped completions, the first + //! one observed wins; subsequent failures are dropped. Concretely: + //! + //! - First child to call @c set_error wins; its error becomes the result. + //! - First child to call @c set_stopped wins (if no error has been seen). + //! - On either, the internal stop source is signalled so the remaining + //! children can wind down promptly. + //! + //! **Cancellation.** + //! + //! @c when_all chains the receiver's stop token to its internal + //! stop-source, so an outer stop request propagates to every child. + //! + //! **Example.** + //! + //! @code{.cpp} + //! #include + //! + //! int main() { + //! using namespace stdexec; + //! auto sched = get_parallel_scheduler(); + //! + //! auto pipeline = when_all( + //! starts_on(sched, just(10) | then([](int x){ return x * 2; })), + //! starts_on(sched, just(5) | then([](int x){ return x + 1; }))); + //! + //! auto [a, b] = sync_wait(std::move(pipeline)).value(); + //! // a == 20, b == 6, computed in parallel on `sched` + //! } + //! @endcode + //! + //! @see stdexec::when_all_with_variant — for inputs with multiple value-completion shapes + //! @see stdexec::transfer_when_all — when_all + scheduler transfer (stdexec extension) + //! @see stdexec::spawn_future — start a sender eagerly and observe via a sender struct when_all_t { + //! @brief Compose @c __sndrs... into a sender that completes when every + //! input has completed. + //! + //! @tparam _Senders A pack of types each satisfying @c stdexec::sender. + //! Must be non-empty. Each must have exactly one + //! @c set_value_t completion signature in the + //! ambient environment. + //! + //! @param __sndrs The senders to compose. Forwarded into the result. + //! + //! @returns A sender that, when connected and started, concurrently + //! starts every input and value-completes with the + //! concatenation of the input's value datums. template constexpr auto operator()(_Senders&&... __sndrs) const -> __well_formed_sender auto { @@ -56,6 +180,40 @@ namespace STDEXEC } }; + //! @brief A variadic sender factory like @c when_all that lifts the + //! "exactly one value completion per input" restriction by + //! wrapping each input in @c into_variant. + //! + //! @c when_all_with_variant is for the case where one or more of the + //! inputs can complete with more than one value-completion shape. Where + //! @c when_all would refuse to compile, @c when_all_with_variant + //! transforms each input via @c stdexec::into_variant first (collapsing + //! that input's multiple shapes into a single + //! @c std::variant, ...> value), and then composes the + //! results with the ordinary @c when_all rules. + //! + //! @code{.cpp} + //! // sndr_a value-completes with either int or std::string; + //! // sndr_b value-completes with float. + //! auto s = stdexec::when_all_with_variant(sndr_a, sndr_b); + //! auto [variant_a, variant_b] = stdexec::sync_wait(std::move(s)).value(); + //! // variant_a: std::variant, std::tuple> + //! // variant_b: std::variant> + //! @endcode + //! + //! See [exec.when.all] in the C++26 working draft for the normative + //! specification (where @c when_all_with_variant is specified alongside + //! @c when_all). + //! + //! **Equivalence.** + //! + //! when_all_with_variant(sndrs...) is specified as + //! expression-equivalent to when_all(into_variant(sndrs)...) + //! (after @c transform_sender), so all of @c when_all's concurrency, + //! error, and cancellation semantics carry over unchanged. + //! + //! @see stdexec::when_all — single-value-completion variant + //! @see stdexec::into_variant — the adaptor that lifts each input struct when_all_with_variant_t { private: @@ -68,6 +226,16 @@ namespace STDEXEC } public: + //! @brief Compose @c __sndrs... into a sender that completes when every + //! input has completed, with each input wrapped in + //! @c into_variant. + //! + //! @tparam _Senders A non-empty pack of types each satisfying + //! @c stdexec::sender. Inputs may have multiple + //! value-completion shapes. + //! + //! @returns A sender equivalent to + //! when_all(into_variant(__sndrs)...). template constexpr auto operator()(_Senders&&... __sndrs) const -> __well_formed_sender auto { @@ -82,8 +250,31 @@ namespace STDEXEC } }; + //! @brief Like @c when_all, but transfers execution to a scheduler before + //! delivering the combined completion. + //! + //! @deprecated @c transfer_when_all is deprecated. It is not part of the + //! C++26 working draft and is retained only for backwards + //! compatibility. Write + //! when_all(sndrs...) | continues_on(sch) instead; + //! the behavior is identical. + //! + //! Composition of @c when_all with @c continues_on: + //! transfer_when_all(sch, sndrs...) is expression-equivalent + //! to continues_on(when_all(sndrs...), sch). The inputs run + //! concurrently (wherever their respective schedulers run them), and + //! once all have completed, the combined result is delivered on + //! @c sch's execution resource. + //! + //! @see stdexec::when_all — without the scheduler transfer + //! @see stdexec::continues_on — the underlying transfer primitive struct transfer_when_all_t { + //! @brief Compose @c __sndrs... and deliver the combined completion on + //! @c __sched's execution resource. + //! + //! @tparam _Scheduler A type satisfying @c stdexec::scheduler. + //! @tparam _Senders A non-empty pack of @c stdexec::sender types. template constexpr auto operator()(_Scheduler __sched, _Senders&&... __sndrs) const -> __well_formed_sender auto @@ -107,8 +298,27 @@ namespace STDEXEC } }; + //! @brief Like @c when_all_with_variant, but transfers execution to a + //! scheduler before delivering the combined completion. + //! + //! @deprecated @c transfer_when_all_with_variant is deprecated. It is not + //! part of the C++26 working draft and is retained only for + //! backwards compatibility. Write + //! when_all_with_variant(sndrs...) | continues_on(sch) + //! instead; the behavior is identical. + //! + //! Composition of @c when_all_with_variant and @c continues_on: + //! transfer_when_all_with_variant(sch, sndrs...) is + //! expression-equivalent to + //! continues_on(when_all_with_variant(sndrs...), sch). + //! + //! @see stdexec::when_all_with_variant + //! @see stdexec::transfer_when_all struct transfer_when_all_with_variant_t { + //! @brief Compose @c __sndrs... (each wrapped in @c into_variant) and + //! deliver the combined completion on @c __sched's execution + //! resource. template constexpr auto operator()(_Scheduler&& __sched, _Senders&&... __sndrs) const -> __well_formed_sender auto @@ -132,9 +342,40 @@ namespace STDEXEC } }; - inline constexpr when_all_t when_all{}; - inline constexpr when_all_with_variant_t when_all_with_variant{}; - inline constexpr transfer_when_all_t transfer_when_all{}; + //! @brief The customization point object for the @c when_all sender factory. + //! + //! @c when_all is an instance of @ref when_all_t. See @ref when_all_t for + //! the full description, completion signatures, error/stop semantics, and + //! a usage example. + //! + //! @hideinitializer + inline constexpr when_all_t when_all{}; + + //! @brief The customization point object for the @c when_all_with_variant + //! sender factory. + //! + //! @c when_all_with_variant is an instance of @ref when_all_with_variant_t. + //! See @ref when_all_with_variant_t for the full description. + //! + //! @hideinitializer + inline constexpr when_all_with_variant_t when_all_with_variant{}; + + //! @brief The customization point object for the @c transfer_when_all + //! sender factory. + //! + //! @deprecated See @ref transfer_when_all_t. Use + //! when_all(...) | continues_on(sch) instead. + //! + //! @hideinitializer + inline constexpr transfer_when_all_t transfer_when_all{}; + + //! @brief The customization point object for the + //! @c transfer_when_all_with_variant sender factory. + //! + //! @deprecated See @ref transfer_when_all_with_variant_t. Use + //! when_all_with_variant(...) | continues_on(sch) instead. + //! + //! @hideinitializer inline constexpr transfer_when_all_with_variant_t transfer_when_all_with_variant{}; namespace __when_all diff --git a/include/stdexec/__detail/__write_env.hpp b/include/stdexec/__detail/__write_env.hpp index ef0f0f2b1..be35a97d3 100644 --- a/include/stdexec/__detail/__write_env.hpp +++ b/include/stdexec/__detail/__write_env.hpp @@ -73,6 +73,75 @@ namespace STDEXEC } }; + //! @brief A pipeable sender adaptor that augments the environment seen by + //! a predecessor sender with additional queries. + //! + //! @c write_env is the inverse of @ref read_env. Where @c read_env *reads* + //! a value from the receiver's environment and exposes it on the value + //! channel, @c write_env *injects* values into the environment a child + //! sender sees — overriding or augmenting what the eventual receiver + //! exposes. + //! + //! You give it a sender and an environment (typically built with + //! @c stdexec::env and @c stdexec::prop); you get back a sender that, + //! when connected, presents the *union* of the supplied environment and + //! the connected receiver's environment to its predecessor. Anything the + //! predecessor reaches for via @c get_env / @c read_env sees the merged + //! view. + //! + //! Both call syntaxes are supported (the second is the *pipeable* form): + //! + //! @code{.cpp} + //! auto s1 = stdexec::write_env(sndr, env); + //! auto s2 = sndr | stdexec::write_env(env); + //! @endcode + //! + //! The supplied environment shadows the receiver's environment for any + //! query the supplied environment can answer; queries it cannot answer + //! fall through to the receiver's environment unchanged. + //! + //! **Common uses.** + //! + //! - Injecting a stop token: sndr | write_env(prop{get_stop_token, my_token}) + //! so a sub-pipeline observes a different cancellation signal than the + //! outer pipeline. + //! - Supplying an allocator: sndr | write_env(prop{get_allocator, my_alloc}) + //! so child operations allocate via @c my_alloc. + //! - Hooking domain customization: a custom scheduler may inject its + //! domain into the environment for senders that don't have a scheduler + //! in their chain. + //! + //! **Completion signatures.** + //! + //! @c write_env preserves the predecessor's completion signatures + //! unchanged. (The predecessor may compute different signatures + //! depending on what's in its environment — so the supplied env may + //! influence which signatures the framework computes — but + //! @c write_env does not itself add or remove any.) + //! + //! **Example.** + //! + //! @code{.cpp} + //! using namespace stdexec; + //! + //! auto inner_sndr = read_env(get_stop_token) + //! | then([](auto tok) { return tok.stop_requested(); }); + //! + //! stop_source src; + //! auto pipeline = inner_sndr + //! | write_env(prop{get_stop_token, src.get_token()}); + //! + //! auto [requested] = sync_wait(std::move(pipeline)).value(); + //! // requested == src.stop_requested(), regardless of the outer + //! // pipeline's stop token. + //! @endcode + //! + //! @see stdexec::read_env — read a value from the environment + //! @see stdexec::env — construct an environment from properties + //! @see stdexec::prop — bind a query CPO to a value + //! @see stdexec::get_env — the CPO that exposes the merged environment + //! + //! @hideinitializer inline constexpr __write_env_t write_env{}; template <>