The Sender Sub-Language

2026-02-28 18:40

The paper The Sender Sub-Language by Vinnie Falco <vinnie.falco@gmail.com> and Mungo Gill <mungo.gill@me.com> makes extensive use of my work at https://github.com/steve-downey/sender-examples. The code is also the basis for my talk at C++Now 2023, Using the C++ Sender/Receiver Framework: Implement Control Flow for Async Processing. They present the code accurately and fairly, and I am very happy they found it useful in describing and understanding the capabilities of Senders in the framework. There is no higher praise than someone finding your work useful to build upon.

Nonetheless, we come to different overall conclusions about the Sender/Receiver framework.

There are several papers in the February 2026 pre-Croydon mailing which are, taken together, asking to reconsider the sender / receiver framework, generally and especially in the context of networking. As this is the meeting in which C++26 will be finalized for publication, this is the last chance for any objections to anything being included. It would definitely have been better to have these objections and any new information earlier, but, since changing anything after the standard is shipped is far more difficult, even sustained opposition with new information is important to consider.

This post, however, is not my consideration of the new papers.

I simply wish to put the work I did in 2022 and 2023 in context.

1. The Papers

The papers which I am looking at

Number Title Author Subgroup
P2583R0 Symmetric Transfer and Sender Composition Mungo Gill, Vinnie Falco LEWG Library Evolution
P4003R0 Coroutines for I/O Vinnie Falco, Mungo Gill, Steve Gerbino LEWGI SG18: LEWG Incubator,LEWG Library Evolution
P4007R0 Senders and Coroutines Vinnie Falco, Mungo Gill LEWG Library Evolution
P4014R0 The Sender Sub-Language Vinnie Falco, Mungo Gill LEWG Library Evolution
P4029R0 The SG14 Priority List for C++29/32 Michael Wong SG14 Low Latency

2. Abstracts and Suggested Polls

These quotes are drawn from their respective papers to provide context.

2.1. P2583R0: Symmetric Transfer and Sender Composition

2.1.1. Abstract

C++20 provides symmetric transfer (P0913R1) - a mechanism where await_suspend returns a coroutine_handle<> and the compiler resumes the designated coroutine as a tail call. Coroutine chains execute in constant stack space. std::execution (P2300R10) composes asynchronous operations through sender algorithms. These algorithms create receivers that are structs, not coroutines. No coroutine_handle<> exists at any intermediate point in a sender pipeline. When a coroutine co_await s a sender that completes synchronously, the stack grows by one frame per completion. P3552R3’s std::execution::task inherits this property. The sender model’s zero-allocation composition property and symmetric transfer’s constant-stack property cannot both be satisfied. One requires structs. The other requires coroutines. This paper describes the mechanism, provides implementation experience, and documents the tradeoff.

2.2. P4003R0: Coroutines for I/O

2.2.1. Abstract

C++20 coroutines have five properties that, taken together, make them uniquely suited to asynchronous I/O: type erasure through coroutine_handle<> , customization through promise_type , stackless independently resumable frames, symmetric transfer through await_suspend , and compiler-managed state that persists across suspension points. Each was designed for generality. Their conjunction yields something no single property suggests: the optimal basis for byte-oriented I/O.

We used C++20 coroutines directly for I/O - timers, sockets, DNS, TLS, HTTP - and observed what the language already provides. The protocol that emerged is the IoAwaitable: a system for associating a coroutine with an executor, stop token, and frame allocator, and propagating this context forward through a coroutine chain to the operating system API boundary where asynchronous operations are performed.

2.2.2. Suggested Straw Polls

SG4 polled at Kona (November 2023) on P2762R2 “Sender/Receiver Interface For Networking”

“Networking should support only a sender/receiver model for asynchronous operations; the Networking TS’s executor model should be removed”

SF F N A SA
5 5 1 0 1

Consensus.

The approach described in this paper - a coroutine-native I/O model using C++20 language features - was not among the alternatives considered.

Poll 1. A coroutine-native I/O model is a distinct approach from both the Networking TS executor model and the sender/receiver model.

Poll 2. New research into coroutine-native I/O, not available at the time of the Kona poll, warrants consideration.

2.3. P4007R0: Senders and Coroutines

2.3.1. Abstract

std::execution serves its domain well. Different asynchronous domains have different costs, and a single model cannot minimize all of them simultaneously. This paper identifies four structural gaps where the sender model meets coroutines: three at the boundary - error reporting, error returns, and frame allocator propagation - and one inside the composition mechanism - the symmetric transfer gap documented in P2583R0. Each gap is the cost of a property the sender model requires for compile-time analysis (P2300R10, P4014R0 ). They are not design defects, they are tradeoffs. Mandating that standard networking be built on the sender model would force coroutine I/O users to pay these costs. A coroutine-native I/O research report (P4003R0) made the gaps visible by showing that partial results, error returns, cancellation, and frame allocator propagation emerge naturally when I/O is designed for coroutines. The findings hold regardless of that report’s specific design. This paper recommends: ship std::execution for C++26, defer task (P3552R3, “Add a Coroutine Task Type”) to C++29, and explore coroutine-native I/O designs alongside sender-based designs.

2.3.2. Suggested Straw Polls

  1. “I/O completions that carry both an error code and a byte count present a design challenge for the three-channel completion model.”
  2. “The coroutine integration in std::execution has open design questions that would benefit from further iteration.”
  3. “WG21 should explore coroutine-native I/O designs alongside sender-based designs.”

2.4. P4014R0: The Sender Sub-Language

2.4.1. Abstract

C++26 introduces a rich sub-language for asynchronous programming through std::execution (P2300R10) . Sender pipelines replace C++’s control flow, variable binding, error handling, and iteration with library-level equivalents rooted in continuation-passing style and monadic composition. This paper is a guide to the Sender SubLanguage: its theoretical foundations, the programming model it provides, and the engineering trade-offs it makes. The trade-offs serve specific domains well. The question is whether other domains deserve the same freedom to choose the model that serves them.

2.4.2. Suggested Straw Polls

  1. “ std::execution serves coroutine-driven async I/O less ideally than heterogeneous compute.”
  2. “Coroutine-driven async I/O should have the same freedom to optimize for its domain as heterogeneous compute did

2.5. P4029R0: The SG14 Priority List for C++29/32

This paper is a forward looking priority list for SG14 Low Latency. It has overlapping concerns about the sender / receiver model with the other papers.

2.5.1. Executors/coroutine backward flow + lazy execution vs direct forward flow model for CPU-bound I/O and Networking.

  1. Decouple Networking from std::execution SG14 advise that Networking (SG4) should not be built on top of P2300. The allocation patterns required by P2300 are incompatible with low-latency networking requirements.
  2. Standardize "Direct Style" I/O Prioritize P4003 (or similar Direct Style concepts) as the C++29 Networking model. It offers the performance of Asio/Beast with the ergonomics of Coroutines, maintaining the "Zero-Overhead" principle.

3. The Sender Model

As described in my earlier papers and talks, and accurately summarized by P4014R0: The Sender Sub-Language, senders are a framework for working with continuations and for supporting Continuation Passing Style, or CPS. The CPS formalism is a full model of computation, Turing complete, and equivalent in expressiveness to Single Static Assignment, the current, common, model that compilers use for lowering programming languages. CPS used to be far more common, and is still in use in some language implementations today. The reasons for choosing one over the other is entirely out of scope for this discussion.

The Sender model is also closely tied to the Cont monad. For background see Control.Monad.Cont, Haskell/Continuation passing style, and The Mother of all Monads. The monad abstracts away the chaining and connection of the results to the eventual receiver. Senders augment the model by adding the three channels used to communicate between the processes, allowing for propagation of values, errors, and cancellation requests throughout the graph.

The Sender / Receiver framework is also a model of Structured Concurrency, Structured Concurrency, Martin Sustrik, Notes on structured concurrency, or: Go statement considered harmful – Nathaniel J. Smith, a system for ensuring that concurrent operations have clear entry and exit points and will all complete before exiting. It does so in part by also using delimited continuations, ones that have fixed, rather than unbounded, end points, Continuations and Delimited Control – Oleg Kiselyov

In evaluating a library, particularly one that will be frozen in the C++ standard, where good libraries go to die, I want to know if what is being offered is complete, and if it is not, if what is offered has everything necessary and sufficient to base further work on outside the library. We made this mistake, for example, with the initial Ranges work where there was no mechanism for a user to write a pipe-able algorithm with what was specified. This was quickly rectified, fortunately. With format, the ability to statically check the format string was back-filled, and was not ABI compatible, causing vendors much distress in shipping the new component.

It was with this in mind that I was looking at what was well specified and available in the original P2300 papers. In general I prefer systems that have firm theory behind them over ones that merely, pragmatically, appear to work. As an industry we were all burned badly by the pthreads library, which appeared to work, but we now know can not by itself. The pthread model is also one that we have demonstrated, repeatedly, that human beings can not reason about. On the other hand, object orientation still has no good theory, but has been demonstrably successful. The abstractions it provides allow humans to reason at a much higher level about the complex systems we are producing.

The work in 2022 and 2023 was with the goal in mind of showing that the end user facing parts of the model, senders and the basic adapters in the proposal, were necessary and sufficient as a basis for further work. That the primitives would not need to be scrapped or changed in ways that would break code once written. This is not the same as saying there need be no other adapters or algorithms provided, or that the sender primitives are the most efficient implementations. The question was how far can the model be pushed without having to work with operational state directly.

The base sender operations, just, then, and let_value, allow all structured programming constructs to be created, including structured concurrency. The model is complete and well founded.

The proposal, as adopted, also provides for user implementation and inter-operation of types that have operational states, senders and sender adapters themselves. Analogously to how the standard allows for user written containers to participate in the containers/iterators/algorithms triad that comprised the original STL proposal. Users may not write new containers or iterators often, but there is no particular magic involved, and writing one should be well within the capabilities of most C++ programmers.

A sender is somewhat more difficult, as it is new so there is less documentation, but it is not intrinsically hard. In addition, the APIs tend to encourage correct and safe use. Senders describe the shape of work and should be generally reusable. Programmers will work with senders and sender adaptors several orders of magnitude more than they will write a sender or sender adapter from scratch.

My question was not, what is missing that can be added. That is self evidently lots of things. We are still adding range algorithms and new ranges after years of work and more are in the pipeline for C++29 already. My question was, can real applications be created with nothing more than what is easily available in senders, and can an application programmer who does not wish to create a sender today, still deliver a sender graph of async work.

That was, and is, clearly, to me, yes.

Exported: 2026-03-01 21:13:43