Review request - P4003R3, "A Minimal Coroutine Execution Model"
Colleagues, I am requesting a formal review of P4003R3, "A Minimal Coroutine Execution Model." It is published in the May 2026 mailing, so it is eligible under the rules, and I would like it read closely before it next goes to LEWG. Please all read the same revision so the summary attaches cleanly to a published artifact: - **Paper under review - P4003R3:** https://wg21.link/P4003R3 - **Design companion - P4172 (rationale, alternatives, preemptive objections):** https://wg21.link/P4172 - **Umbrella - P4100 (the Network Endeavor):** https://wg21.link/P4100 - **Implementations:** Capy (https://github.com/cppalliance/capy) and Corosio (https://github.com/cppalliance/corosio) - **Self-contained demo:** https://godbolt.org/z/Wzrb7McrT What the paper proposes The paper asks LEWG to advance the *IoAwaitable* protocol as a standard coroutine execution model. It starts from the smallest possible use case, `co_await f();`, and observes that for this to work something must decide three things at the moment a coroutine suspends: 1. Who resumes it, and where (executor affinity). 2. Whether it should stop (stop token propagation). 3. Where its frame is allocated (frame allocator delivery). The protocol provides exactly those three things and nothing more. It is positioned as a companion to `std::execution` (P2300), not a replacement: each serves the domain where its design choices pay off. What I am asking you to evaluate Reviews here are informational. There is no verdict and no gate, so please do not feel you are voting the paper up or down. What I would most value is an honest read on four questions. **1. Is it worth standardizing?** Does this belong in the standard, or can it live as a library? The central claim is that a small normative protocol unlocks a large amount of interoperable user code (the "What We Get" table in Section 2). Is that interoperability argument convincing? Is the benefit worth the committee time and the standard surface it would consume? **2. Did the use of AI affect your perception?** This paper, and the review program you are reading this on, were developed with heavy AI assistance, and I am not hiding that. I want to know candidly whether knowing or suspecting that changed how you read it. Did it raise or lower your trust? Did you go looking for tells, and did you find any? Did it make you more or less rigorous than you would be with a paper written entirely by hand? I would rather you name this in your review than leave it unsaid. The answers matter as much to me as the technical feedback, and they will shape how the Network Endeavor and this program handle AI-assisted work. **3. What is the technical quality?** Are the concepts sound and precisely stated? Do the central claims hold up - zero per-operation allocation under type erasure, symmetric-transfer resumption, automatic frame-allocator propagation, and the structured-concurrency argument in Section 5? Is the implementation evidence (Capy, Corosio, the benchmark table, the Compiler Explorer demo) credible and reproducible? Where is the reasoning weakest? **4. What is missing?** What would you need to see before LEWG could act on this? I am looking for gaps in specification or wording, unaddressed edge cases, missing comparisons (against existing coroutine task libraries, or other parts of `std::execution`), lifetime or cancellation corners, threading hazards, and anything the paper moves past too quickly. Tell me what you went looking for and did not find. Thanks
Up front it is worth stating that networking is not my domain of expertise.
**1. Is it worth standardizing?** Does this belong in the standard, or can it live as a library? The central claim is that a small normative protocol unlocks a large amount of interoperable user code (the "What We Get" table in Section 2). Is that interoperability argument convincing? Is the benefit worth the committee time and the standard surface it would consume?
On the whole this does seem to be worth standardizing based on the interoperability claim. Something that would strengthen the argument would be a case study from a deployment where using this protocol reduced friction/improved performance/etc since neither Capy nor Corosio have long term feedback like ASIO.
**2. Did the use of AI affect your perception?**
There are a few instances of clear AI tells such as, "The left column is small. The right column is not. Small protocol, big rewards. It earns its keep.". I wouldn't say that the use of AI hurts my perception, but I would recommend removing obvious AI fluff.
**3. What is the technical quality?** Are the concepts sound and precisely stated? Do the central claims hold up - zero per-operation allocation under type erasure, symmetric-transfer resumption, automatic frame-allocator propagation, and the structured-concurrency argument in Section 5? Is the implementation evidence (Capy, Corosio, the benchmark table, the Compiler Explorer demo) credible and reproducible? Where is the reasoning weakest?
I'm a bit confused in your abstract where theres the statement, "Everything in this paper comes from a complete implementation on three platforms: Capy(protocol) and Corosio". Is this supposed to mean that Capy and Corosio can run on three platforms? What are they? In section 3.5 on allocation there's only reference to two platforms (MSVC and Apple Clang). Not having GCC on Linux is surprising. I appreciate the inclusion of a working proof of concept on Godbolt. I expect that if there are any technical problems in either of the libraries they will be surfaced before the upcoming Boost review. Overall I would say that the concept of the IoAwaitable protocol seems to be sound.
**4. What is missing?**
It's not actually clear to me what the specification or wording are. For example in section 4.6 You describe what an IoRunnable is and at the end of the section there is a code block for concept IoRunnable. Is that block an example of what you are proposing? I'd recommend having a consolidated section of the entire protocol toward the end with all of the concepts, types, normative wording, etc. before introducing the straw poll voting to advance said protocol. Matt
Hi,
I am requesting a formal review of P4003R3, "A Minimal Coroutine Execution Model." It is published in the May 2026 mailing, so it is eligible under the rules, and I would like it read closely before it next goes to LEWG.
**1. Is it worth standardizing?** Does this belong in the standard, or can it live as a library? The central claim is that a small normative protocol unlocks a large amount of interoperable user code (the "What We Get" table in Section 2). Is that interoperability argument convincing? Is the benefit worth the committee time and the standard surface it would consume?
Yes. This is the foundation of having coroutines with the right semantics for I/O (without the "everything here wants to eat me" feeling you have with Asio). My experience with Capy/Corosio has been positive regarding simplicity and efficiency. Many basic pieces of functionality (I/O) can be built on this, and having one standard model for it is positive.
**3. What is the technical quality?** Are the concepts sound and precisely stated? Do the central claims hold up - zero per-operation allocation under type erasure, symmetric-transfer resumption, automatic frame-allocator propagation, and the structured-concurrency argument in Section 5? Is the implementation evidence (Capy, Corosio, the benchmark table, the Compiler Explorer demo) credible and reproducible? Where is the reasoning weakest?
All the concepts are sound but the paper is extremely not self-contained. I happen to know about these concepts and could follow, but I doubt people that haven't used Capy can get much from this paper without reading p4172r0 [1] first. As an example, consider on_work_started/on_work_finished, required by the proposed Executor. These are used for work counting (prevent io_context::run() from returning until every operation has finished). There is no mention to this concept, yet the functions are there. I haven't found anything on p4172r0 either (although I haven't read it entirely). Maybe this is how these papers work. I don't know. But if I were a reader, I'd prefer external references for details, not for the semantics of the thing I'm being asked to standardize or not. Section 5.1 shows a counting_scope implementation based on run_async() and its handlers, then states that capy::when_all() is a full implementation of that approach. To me, this suggests that when_all() is implemented in terms of run_async() and its handlers, which I don't think is true. Also, all the counting_scope functions are marked noexcept, which I don't think should be. Section 3.5 includes a benchmark table, but it doesn't say what you are benchmarking, under which conditions, or what code could be used to reproduce it. Section 6's table includes the "preallocated awaitable" term, which I don't think is well-known. I assume it refers to an optimization in any_read_stream and friends. Some explanation would be useful. I'm not convinced by how section 3.1 frames the problem. IMO "co_await f();" doesn't immediately ring a bell on executor affinity. "co_await stream.read_some(buf)" does. I'd go something like: - You call co_await stream.read_some(buf) - The platform reactor (event loop) starts the work. - The operation completes. The event loop may be multi-threaded, and there is no guarantee on which thread will dispatch the completion. - BUT we have our protocol, and that saves the day. Just my 2 cents.
**4. What is missing?** What would you need to see before LEWG could act on this? I am looking for gaps in specification or wording, unaddressed edge cases, missing comparisons (against existing coroutine task libraries, or other parts of `std::execution`), lifetime or cancellation corners, threading hazards, and anything the paper moves past too quickly. Tell me what you went looking for and did not find.
I'd say include the minimal semantics of what you're asking the readers to standardize. Also, the paper seems to contain no wording for the standard - should it? I'm no expert in the standardization process.
**2. Did the use of AI affect your perception?** This paper, and the review program you are reading this on, were developed with heavy AI assistance, and I am not hiding that. I want to know candidly whether knowing or suspecting that changed how you read it. Did it raise or lower your trust? Did you go looking for tells, and did you find any? Did it make you more or less rigorous than you would be with a paper written entirely by hand? I would rather you name this in your review than leave it unsaid. The answers matter as much to me as the technical feedback, and they will shape how the Network Endeavor and this program handle AI-assisted work.
I intentionally deferred this to the last point. Yes, negatively. Aside from the LLM-isms that Matt already pointed out, my impression is that this was written by an LLM with a lot of context on the Capy project, and it assumes that the reader also has this context. IMO this makes reading it less enjoyable. I find LLMs incredibly useful for error checking and text improvement (also because I'm not a native English speaker), but I get frustrated when generating content because of this kind of thing. Regards, Ruben. [1] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r0.pdf
В письме от четверг, 18 июня 2026 г. 21:17:00 MSK пользователь Vinnie Falco написал:
**1. Is it worth standardizing?** Does this belong in the standard, or can it live as a library? The central claim is that a small normative protocol unlocks a large amount of interoperable user code (the "What We Get" table in Section 2). Is that interoperability argument convincing? Is the benefit worth the committee time and the standard surface it would consume?
It does to me. But I'm not sure this is because of the paper, and because of my prior exposure to the things it describes.
**2. Did the use of AI affect your perception?** This paper, and the review program you are reading this on, were developed with heavy AI assistance, and I am not hiding that. I want to know candidly whether knowing or suspecting that changed how you read it. Did it raise or lower your trust? Did you go looking for tells, and did you find any? Did it make you more or less rigorous than you would be with a paper written entirely by hand? I would rather you name this in your review than leave it unsaid. The answers matter as much to me as the technical feedback, and they will shape how the Network Endeavor and this program handle AI-assisted work.
Use of AI did not affect my perception, but, consider this line in section 2.
The left column is small. The right column is not. Small protocol, big rewards. It earns its keep.
This is written as an artistic advertisement (something that LLMs often do). I don't think this style is warranted in a standards proposal.
**3. What is the technical quality?** Are the concepts sound and precisely stated? Do the central claims hold up - zero per-operation allocation under type erasure, symmetric-transfer resumption, automatic frame-allocator propagation, and the structured-concurrency argument in Section 5? Is the implementation evidence (Capy, Corosio, the benchmark table, the Compiler Explorer demo) credible and reproducible? Where is the reasoning weakest?
**4. What is missing?** What would you need to see before LEWG could act on this? I am looking for gaps in specification or wording, unaddressed edge cases, missing comparisons (against existing coroutine task libraries, or other parts of `std::execution`), lifetime or cancellation corners, threading hazards, and anything the paper moves past too quickly. Tell me what you went looking for and did not find.
I feel like the split into this paper and P4172 is detrimental. This paper is not a wording proposal, supposedly it needs to convince me that the design it outlines is the necessary minimum. But most of the explanation (design rationale, comments on alternative approaches, etc.) is in another paper.
On 6/18/2026 8:17 PM, Vinnie Falco wrote:
Colleagues,
I am requesting a formal review of P4003R3, "A Minimal Coroutine Execution Model." [...]
What I am asking you to evaluate
[...]
**1. Is it worth standardizing?** Does this belong in the standard, or can it live as a library? The central claim is that a small normative protocol unlocks a large amount of interoperable user code (the "What We Get" table in Section 2). Is that interoperability argument convincing? Is the benefit worth the committee time and the standard surface it would consume?
The main reason to standardize is real. Today, each coroutine library defines its own task type, and tasks from different libraries cannot be mixed together. The companion paper answers how this differs from `std::execution`. So, yes, I think the benefit is worth the committee time and standard surface consumption, provided the open questions below are addressed. I'm just not sure the protocol is "minimal" :-).
**2. Did the use of AI affect your perception?** This paper, and the review program you are reading this on, were developed with heavy AI assistance, and I am not hiding that. I want to know candidly whether knowing or suspecting that changed how you read it. Did it raise or lower your trust? Did you go looking for tells, and did you find any? Did it make you more or less rigorous than you would be with a paper written entirely by hand? I would rather you name this in your review than leave it unsaid. The answers matter as much to me as the technical feedback, and they will shape how the Network Endeavor and this program handle AI-assisted work.
Knowing it was AI-assisted made me more careful, ignoring the confident tone and checking each claim. But I found no invented APIs and no fake citations.
**3. What is the technical quality?** Are the concepts sound and precisely stated? Do the central claims hold up - zero per-operation allocation under type erasure, symmetric-transfer resumption, automatic frame-allocator propagation, and the structured-concurrency argument in Section 5? Is the implementation evidence (Capy, Corosio, the benchmark table, the Compiler Explorer demo) credible and reproducible? Where is the reasoning weakest?
The concepts are written as real C++ and are mostly clear. About the central claims: - Zero per-operation allocation under type-erasure: holds up; appendix B of the companion shows working code for it. The comparison table is set up in a way that favors coroutines, but the point is valid. - Symmetric-transfer resumption: correct and standard. Minor point: there's a small runtime check, so "zero overhead" is slightly too strong. - Automatic frame-allocator propagation: I'm concerned about the method (thread-local pointer that is saved and restored). IIUC, it only works if every place that resumes a coroutine remembers to save and restore the pointer. Nothing forces this, and one mistake means a frame is allocated from the wrong allocator with no error reported. Is my understanding correct? - Structured concurrency (section 5): the paper says the protocol *is* structured concurrency, but it also allows an unstructured fire-and-forget launch. It also says cancellation is automatic, but its own examples require the programmer to check the stop token manually. Again, I may be missing something here. As to reproducibility, I ran the Compiler Explorer demo, but could not find the benchmark sources.
**4. What is missing?** What would you need to see before LEWG could act on this? I am looking for gaps in specification or wording, unaddressed edge cases, missing comparisons (against existing coroutine task libraries, or other parts of `std::execution`), lifetime or cancellation corners, threading hazards, and anything the paper moves past too quickly. Tell me what you went looking for and did not find. I wonder about the following:
- Who owns the `io_env` in a fire-and-forget launch. The stated lifetime rule does not cover this case. - The cancellation details: how a stop request reaches a running operation, and how the race between normal completion and cancellation is handled. - An example that shows how this protocol would solve the interoperability problem among existing coroutine task libraries. - The memory cost of `std::stop_token` compared with an in-place stop token. -- Gennaro Prota <https://prota.dev>
On Mon, Jun 22, 2026 at 6:36 AM Gennaro Prota <gennaro.prota@gmail.com> wrote:
- Automatic frame-allocator propagation: I'm concerned about the method (thread-local pointer that is saved and restored). IIUC, it only works if every place that resumes a coroutine remembers to save and restore the pointer. Nothing forces this, and one mistake means a frame is allocated from the wrong allocator with no error reported. Is my understanding correct?
Thank you so much for your review, and I think this is worth expanding on. You are correct that an implementation mistake means the wrong allocator is used. However, it is important to recognize there are three cohorts of implementors (described in P4172). Ordinary users are not the ones implementing the coroutine machinery (where the TLS save/restore code lives). It is experts implementing this, and they only have to get it right once in the component and then it works forever. Mistakes here are unlikely, and when they surface they will be fixed promptly. Compare this with the approach that passes the allocator as a parameter to every function which returns a task. That places the burden on every ordinary user, at every function in the call chain. Mistakes here are practically guaranteed. And they will go unnoticed because failing to get the allocator right usually only results in performance loss. Thanks
Hi everyone,
**1. Is it worth standardizing?** Does this belong in the standard, or can it live as a library? The central claim is that a small normative protocol unlocks a large amount of interoperable user code (the "What We Get" table in Section 2). Is that interoperability argument convincing? Is the benefit worth the committee time and the standard surface it would consume?
I believe it is. The main reasons C++20 coroutines never got the adoption they deserved is that coroutine types don't interoperate across ecosystems, Boost.Asio being the obvious example. There have been attempts to address this, std::task<T> for instance, but standardizing a coroutine type just relocates the interoperability problem instead of removing it: anyone not using that type is still locked out, and the ecosystems that matter for I/O won't abandon their own types to adopt it. So I think this is heading in the right direction. The common language between C++20 coroutine types should be a protocol, not a single standardized type.
**2. Did the use of AI affect your perception?** This paper, and the review program you are reading this on, were developed with heavy AI assistance, and I am not hiding that. I want to know candidly whether knowing or suspecting that changed how you read it. Did it raise or lower your trust? Did you go looking for tells, and did you find any? Did it make you more or less rigorous than you would be with a paper written entirely by hand? I would rather you name this in your review than leave it unsaid. The answers matter as much to me as the technical feedback, and they will shape how the Network Endeavor and this program handle AI-assisted work.
In my view, using AI assistance to help with wording is perfectly reasonable. However, writing a standards paper requires such a high level of scrutiny and domain expertise that the effort spent on the technical content, analysis, and review typically far outweighs the effort spent on the writing itself.
**3. What is the technical quality?** Are the concepts sound and precisely stated? Do the central claims hold up - zero per-operation allocation under type erasure, symmetric-transfer resumption, automatic frame-allocator propagation, and the structured-concurrency argument in Section 5? Is the implementation evidence (Capy, Corosio, the benchmark table, the Compiler Explorer demo) credible and reproducible? Where is the reasoning weakest?
I'm not sure if std::stop_token is the best fit for cancellation in a coroutine library, and the main reason is that it is designed around the assumption that cancellation is relatively rare. In that sense, it's somewhat analogous to the distinction between exceptions and std::error_code/std::expected. std::stop_token is structured more like an exception mechanism: cancellation is relatively expensive to trigger, with the design optimized for the case where stopping is exceptional. In racing compositions, however, cancellation is often the expected outcome of normal execution. For example: auto [ec, r] = co_await wait_any(socket.connect(ep1), socket.connect(ep2)); On every successful completion, one of the connect operations is cancelled. Since this is part of the normal control flow, it seems desirable for the associated costs to be minimal and predictable, rather than optimized around the assumption that cancellation is uncommon. Because of that, I don't think std::stop_token is an ideal choice here: 1. Thread safety is unconditional. Although it is implemented using atomics, those typically compile to lock-prefixed RMW instructions (lock xadd, lock cmpxchg) that acquire exclusive ownership of the cache line and act as full memory fences. As a result, even a single-threaded or strand-based executor pays for synchronization guarantees it never actually needs. 2. std::stop_source requires a heap-allocated control block created via a regular new. By contrast, a coroutine frame already provides stable storage that could hold cancellation state inline, avoiding the allocation entirely.
**4. What is missing?** What would you need to see before LEWG could act on this? I am looking for gaps in specification or wording, unaddressed edge cases, missing comparisons (against existing coroutine task libraries, or other parts of `std::execution`), lifetime or cancellation corners, threading hazards, and anything the paper moves past too quickly. Tell me what you went looking for and did not find.
The IO prefix in the concept names seems unnecessarily restrictive. The model appears applicable to non-I/O tasks as well, so a more general name might better reflect its intended scope. Regards, Mohammad
Hi, Disclosure: I gave feedback on this work at an earlier stage via direct email, so some of this echoes points I raised before. Overall, these are solid features, and the papers are well written. Extending coroutines to close the gaps C++20 left around affinity and cancellation is genuinely important work, and I am glad to see it. The motivation chapters are among the strongest parts, "Complicated Success" in particular, and the compound-result argument is compelling. **1. Is it worth standardizing?** Yes, the underlying problem is real and worth solving. Had these papers been submitted three years ago, they would likely have been accepted with no major issues. The difficulty is that we now have `std::execution` in C++26, and proposing a second model for structured asynchronism is a much harder sell today. The papers motivate *why* a second model is needed and why `std::execution` struggles with byte-movement requirements, and the `await_sender`/`as_sender` interop helps, but I do not think it is yet enough to carry LEWG. My biggest concern here is that the proposal duplicates a large amount of synchronization and pipeline machinery, and that will be treated as a serious issue in LEWG. The domain argument justifies a second *execution model*. It does not obviously justify a second *set of algorithms*. Re-deriving `when_all`, `when_any`, `on_timeout`, and the rest for two independent frameworks is the kind of thing the room will resist strongly. The paper should demonstrate whether the existing `std::execution` algorithms can be customized to run with no overhead over a custom `io_scheduler`, and if they cannot, state exactly why. Without that analysis the fork will not survive LEWG. The same applies to the types: - A second `task` is likely a no-go, as C++26 already took `std::execution::task`. The paper should show whether the accepted C++26 `task` can be made to satisfy the IoAwaitable protocol with no overhead (for example via promise/allocator customization), and justify a separate type only if it cannot. If a separate type does turn out to be unavoidable, please name it `io_task` at minimum. - `std::execution` already has `system_context`/`parallel_scheduler`, yet P4100 adds both `thread_pool` and `system_context`. The paper should discuss whether one generic type can serve both `std::execution` and io_awaitables with no overhead, and if it cannot, why a separate type is required. So: worth standardizing in principle, but the duplication needs to be reduced or very strongly justified before it advances. **2. Did the use of AI affect your perception?** Yes, somewhat. A few passages read as marketing copy in a register that is not WG21 paper style, and others in this thread have pointed at the same lines. More importantly, the paper is simultaneously long and too brief. A lot of the design needed to understand what is going on is missing, forcing the reader to reconstruct it across multiple papers. That is frustrating, and a proposal like this should be self-contained. My impression is that the material was organized in a way that is easy for an AI to parse and reason over all at once, but a human reader does not have that luxury. (Ruben and Matt make essentially the same observation.) **3. What is the technical quality?** The concepts are mostly sound and clearly stated as real C++. My chief worry is the automatic propagation of the frame allocator in "Out of band" mode, which the paper seems to prefer. Relying on thread-local (or static) storage for the allocator is unlikely to be welcomed in LEWG, and I would expect heavy discussion on it. The paper currently seems to sidestep the issue rather than confront it. Please add an explicit, well-argued rationale rather than leaving the mechanism implicit, covering correctness under arbitrary resumption points, who is responsible for save/restore, and what happens when it goes wrong. (Gennaro raised the same save/restore correctness concern, and it deserves a direct answer in the paper.) A few more specific technical questions: - Is `nullptr` a valid value for the `env` argument of `await_suspend(h, env)`? The "borrowed pointer" rationale is fair, but if `env` is always non-null, a reference expresses exactly that (borrowed, never null) and keeps the compile-time boundary check. If null *is* meaningful, the protocol should say so, because the P4251 examples dereference `env->executor` unconditionally. - `Executor` concept: - (a) `std::equality_comparable<E>` could replace `{ ce == ce2 } noexcept -> std::convertible_to<bool>`, though you would need to add the `noexcept` back, since Asio-style executors want it. - (b) `dispatch` requiring `same_as<std::coroutine_handle<>>` means a concrete executor returning a specific handle type will not satisfy it. I would use `convertible_to<std::coroutine_handle<>>` or an exposition-only `specialization-of<std::coroutine_handle>`, unless the erased handle is a deliberate symmetric-transfer contract, in which case please say so. - (c) `on_work_started()`/`on_work_finished()` are mandatory in the concept. Should they be? - `executor_ref(E const&)` is non-explicit and stores `&e`, which is a dangling hazard when constructed from a temporary. An `explicit` constructor plus a deleted rvalue overload would help. - `execution_context::service` is a polymorphic base but does not delete copy/move. It should be non-copyable (and likely non-movable) to avoid slicing. - `IoRunnable` requires `typename T::promise_type` directly, which is too constraining. It should go through `std::coroutine_traits<T>::promise_type`, the language's actual customization point. **4. What is missing?** Beyond the consolidated normative section already mentioned, here are the items I looked for but did not find. *Naming.* `io_stream` collides conceptually with `istream`/`ostream` and `<iostream>`. A `std::io` namespace would resolve several of these at once. You already show it in P4100 §13 (`std::io::task`, `std::io::any_stream`), so I would commit to it consistently and drop the redundant `io_` prefixes. (Mohammad's "IO prefix is unnecessarily restrictive" points the same way.) Also: `run_async()` spawns a detached coroutine, which is potentially harmful. If multiple detached launches before `ctx.run()` are intended, `schedule()` or `spawn()` reads better, since `run_async()` does not actually run anything. We suspend at the initial suspend, correct? And `run()` reads like a blocking call, but it is the in-coroutine operation that switches executor/stop-token/allocator and is always `co_await`-ed. That is `on`/`continue_on`/`with_context`, with `with_context()` the best fit. Note that P4100 §8.2 calls `run()` "blocks until the task completes," contradicting the other papers. One of those needs fixing. The papers also use "executors" (pre-P2300) where `std::execution` has "schedulers." Two vocabularies for adjacent ideas are a teachability cost. If the Asio-style `Executor` (dispatch/post/context) is kept deliberately, please own that cost explicitly. If teachability is not discussed anywhere, it should be. See P3045R9 for examples. *Adapters/interop gaps.* The explicit `as_sender`/`await_sender` is great. An implicit path would be nice too. Could `io_task`'s `await_transform` recognize a sender and apply `await_sender` under the hood, so the bridge is as invisible as today's coroutine/sender interop? And could `std::execution` abstractions be taught to treat io_awaitables like regular coroutines, running `as_sender`/`await_sender` internally? Also missing: how to await an io_awaitable from a *regular* (non-IO) coroutine. Not every coroutine will be an `io_task`, yet people may still need to wait on an IO event asynchronously. Is `run()` meant to cover that? *Cancellation.* Should `run_async` take a cancellation handler? It currently takes `(executor, value, error)` in fixed order with no stopped/cancel channel. Could the handler order be deduced from the concepts the callables satisfy, so a caller who only cares about cancellation or error need not pass a dummy value continuation? *Shape of `IoRunnable`.* It exposes `release()`, `handle()`, `exception()`, `set_continuation()`, and `set_environment()` on the public surface even though they are only used inside the launch machinery. Users may call them incorrectly (for example `release()`) in coroutine code. A coroutine-shell approach like my `sync_await`/`synchronized_task` (a shell over a regular `co_await`) hides this at the cost of one allocation: https://github.com/mpusz/mp-coro/blob/main/src/include/mp-coro/sync_await.h. Relatedly, is there a `sync_await`-style blocking entry point returning a value to `main()`, and will it be customized for io_awaitables? *ABI stability.* The papers repeatedly stress that no recompilation of type-erased code is needed. Is that mainly a differentiator from `std::execution`, or a genuinely strong industry requirement? I am asking sincerely. If it is the latter, consolidate the evidence (field report, Boost adopters) in one place and quantify it, and separate "ABI stability is valuable" from "therefore a coroutine-native model is required to get it." *Process / strategy.* This is not about the paper itself, but about how to advance it. I would strongly recommend securing SG4 (Networking) buy-in before LEWG. SG4 understands the networking reality and is best placed to judge where `std::execution` falls short here. Strong SG4 backing makes LEWG progress much easier, whereas going to LEWG first risks a wall that is hard to recover from, even with later SG4 support. (I take my own unit-library papers to SG6 first for the same reason.) I would also expect the relevant chairs to want you and Dietmar Kühl to converge on a common approach before LEWG acts, so doing that early is worthwhile. If you do go to LEWG directly, you may want to request an evening session to build an audience and find supporters ahead of the main-room discussion, and focus it on "Why?" rather than "How?" That sequencing has worked well for me. Thanks for putting this together. I hope the feedback above is useful. Mateusz
On Wed, Jun 24, 2026 at 7:27 AM Mateusz Pusz via Paper-reviews < paper-reviews@lists.wg21.org> wrote:
- A second `task` is likely a no-go, as C++26 already took `std::execution::task`.
Yes this is a potential problem and I write about it in this paper: *Why I Write* https://isocpp.org/files/papers/P4263R0.pdf Do note that P4003 does not propose a task type. It only propose IoAwaitable, and a couple of other concepts and concrete types. It is the minimum amount needed for a coroutine-native I/O foundation. Thanks
Thank you writing this interesting paper and putting in the work to advance the state of C++. I am not an expert in networking. I am not an expert in coroutines. I am somewhat an expert in academic writing. So I will not answer questions I'm not qualified to answer, and instead give my two cents about the form of the argumentation that is IMO the most serious hindrance towards reading and acceptance. The abstract does not follow the standard rule of guiding new readers toward the problem at hand. I understand standard papers are terser than academic, but terse is no excuse for context-free. Usually the abstract should funnel the reader progressively from broad context to the specific contribution. The present abstract does the opposite: - no opening content - it drops us straight into the co_await f() code, - no problem statement and no indication why we should care - It does not mention whether the problem exists in the real world - and does not explain why the committee should spend time on it A more useful abstract could look like this (I'm no networking expert, so please forgive probable mistakes, it's just to illustrate my point:
(broadest context) C++20 coroutines are now widely used for asynchronous code, but the language deliberately left the execution model unspecified. The standard library provides no common vocabulary for the three decisions every suspended coroutine must make.
(narrowing) As a result, every coroutine library reinvents incompatible specifications of how a coroutine decides which thread it resumes on, whether it should be cancelled, and where its frame memory comes from. This fragmentation prevents tasks, awaitables, executors, and launch functions from different libraries from interoperating, and it leaves frame allocation (the dominant coroutine cost) without a standard customization point. (narrowing further to the specific method, high level vision) This paper proposes the IoAwaitable protocol: a minimal coroutine execution model supplying exactly three things: executor affinity, stop-token propagation, and frame-allocator delivery. (specific method, low level vision: precise contribution, bring evidence) The protocol is small (a handful of concepts: IoAwaitable, IoRunnable, Executor, execution_context) yet enables interoperable tasks, executors, and launch functions across libraries. It is a companion to P2300 std::execution rather than a competitor. The design has a complete implementation on three platforms, with measured frame-allocator speedups up to 3.1×. This paper asks LEWG to advance it as a standard coroutine execution model. <<< Many terms are used but not defined, even loosely: - interoperable tasks - frames - executor affinity - stop token propagation etc etc One could argue those are such basic terms in the context of this paper - and I'd agree. However, one clear thing from my understanding of standardization meetings is that people are often not experts in the specific problems being treated. Lowering the friction for them to understand your contribution's value in their context is strategically worth your time if you want your proposal to advance. Without becoming a 101 class on coroutines, a good introduction must give intuition about the most fundamental aspects the paper proposes to advance. It goes hand in hand with making the paper more self-contained, as previously mentioned. Defining terms briefly in the introduction and/or providing a more detailed list in a glossary at the end are standard ways to ensure people follow and use the same definitions as you do, reducing useless debate around terminology.
The left column is small. The right column is not. Small protocol, big rewards. It earns its keep.
Value claims should be argued and demonstrated, not sloganized. This should not pass editing.
See P4172R0 [1] for design rationale and analysis of alternativeapproaches
Like in programming, pulling too many references increases the reader's cognitive load, fatigue and frustration. Consider whether a quick summary of those papers (take home message) could advance your point more efficiently. Several passages read as over-compressed, to the point of limiting readability and impacting understanding:
What follows is the minimum. What follows is the minimum as well.
This is not an efficient way to introduce an entire section. I would recommand giving a high-level vision of what will be treated in the three sub-sections and why. Readers must be prepared for what follows, otherwise they will reach the end of the subsections overwhelmed by technical details disconnected from the high-level understanding and stakes.
The minimal statement that suspends a coroutine:
It lacks context: is it proper to this proposal, is it a generally agreed on design, what is the rationale, etc.
But on which thread? Under whose control? (duplicated in code snippet)
The rhetorical-question register is inappropriate for a normative proposal, and it is repeated verbatim in the code comment.
Executor affinity, stop token propagation, frame allocator delivery. Three concerns. One protocol. A companion to std::execution, implemented on three platforms. Nothing in it can be removed.
This is not a proper conclusion, this is a slogan. And it risks delegitimizing the paper. A conclusion should tell the reader what to take away now that they have the full argument. In its current form: someone who read the paper learns nothing new, and someone who did not read the paper learns nothing. "Nothing can be removed" is a minimality claim that can be proved true or false but was never justified/explored in the paper.The paper asks for advancement from LEWG, but the conclusion dodges this request. The conclusion is the natural place to state simply: i) what advancing this enables, ii) what fragmentation costs if you don't, and iii) why this is the right minimal layer to standardize now and not later. A proper conclusion should funnel the reader out: - restate the contribution - widen to significance and consequences - what does C++ gain if you accept it, and what does it lose if you don't - who builds on it - what's the relationship to the wider context (frameworks and libraries, std::execution etc) Overall the paper: - reflects a lot of work behind the scenes - is not self-contained and will frustrate the reader - does itself a disservice by emptying sections of rationale and justification: the reader is expected to subliminally accept that this proposal is superior to alternatives - does not walk people in, does not walk people out - reads in places as heavily compressed, possibly auto-summarized, at a real cost to readability Thank again for giving us this interesting opportunity to review this work. I am excited about seing what comes next. Best wishes, Arnaud Becheler
On Thu, Jun 18, 2026 at 11:17 AM Vinnie Falco <vinnie.falco@gmail.com> wrote:
...
I would like to thank everyone for reviewing this paper. There are enough reviews now, and the feedback has been very informative. I have a strong set of improvements to make based on what has been said here. Regeards
El 18/06/2026 a las 20:17, Vinnie Falco escribió:
Colleagues,
I am requesting a formal review of P4003R3, "A Minimal Coroutine Execution Model." It is published in the May 2026 mailing, so it is eligible under the rules, and I would like it read closely before it next goes to LEWG.
I sincerely find the paper lacking too much context for a reader like me that is not an expert on coroutines. If the paper is not designed for people like me, fine, but it might indicate some context/info is missing in the paper. I've taken the latest revision (P4003R4) to write some comments: ------------------------------------------------------------ ------------------------------------------------------------ 1) Defining a bit more what's offered in the introduction. ------------------------------------------------------------ ------------------------------------------------------------ I can deduce from the name capitalization and colors something, but in in section "What std provides", it should say if the protocol talks about a concept or a class probably (and "What std provides" is a bit: Type Name What users can write concept IoAwaitable /**/ .... class execution_context /**/ .... If IoAwaitable is proposed for the standard, then it should be written as other standard concepts ------------------------------------------------------------ ------------------------------------------------------------ 2) I miss the "purpose" of each name and which C++ actor will use it ------------------------------------------------------------ ------------------------------------------------------------ std::io_env / std author writes it / carried to every suspension point of IoAwaitable types in a.await_suspend(h, env). IoAwaitable / A type A such that a.await_suspend(h, env) is valid / std provides the concept and library authors write awaitables that satisfy the concept (e.g. a socket's read_some) IoRunnable / A concept refining IoAwaitable for It's the contract between the return type of a coroutine and a launcher function that need access to the environment. / std provides the concept and authors write couroutine return types (e.g. task<T>) that satisfy it [....] ------------------------------------------------------------ ------------------------------------------------------------ 3) In "IoRunnable and Launch Functions" it is not clear what type requires IoRunnable. ------------------------------------------------------------ ------------------------------------------------------------ Types returned from "server_main()" and "compute()" in the run_async example, etc. It's confusing what "run_async" and "run" are, they are examples of launchers that need to modify the environment, but they are introduced with much information to understand them ------------------------------------------------------------ ------------------------------------------------------------ 4) Some text repetitions: ------------------------------------------------------------ ------------------------------------------------------------ There are repetitions like "Without handlers, the result is discarded and exceptions rethrow". The IA could catch them and rewrite them not to make small paper repetitive ------------------------------------------------------------ ------------------------------------------------------------ 5) Novel concepts without context ------------------------------------------------------------ ------------------------------------------------------------ The paper defines important concepts like "reactor" without any context. I find this makes as a not solid grounded for non-experts like me. I can imagine what "reactor" could mean but I feel lost. ------------------------------------------------------------ ------------------------------------------------------------ 6) Basic concepts are missing ------------------------------------------------------------ ------------------------------------------------------------ And now my main objection (taking into account my little knowledge about coroutines in theory and in practice, please be patient. I feel the facility presentation lacks enough context/concepts to connect all facilities. Example: the IoAwaitable concept requires a signature that it's different from the standard coroutine machinery that calls await_suspend with a single argument (non type-erased handle). The paper says "The caller's await_transform injects the environment as a pointer parameter - no templates, no type leakage", but there is IMHO an important missing point. For someone that has very basic coroutine knowledge, it isn't clear how this happens in the protocol. The language's contract for "await_suspend" is fixed and one-argument. How is that translated to the two-argument await_suspend required by the IoAwaitable written by the user? The two-argument form in the IoAwaitable concept is a private convention between the promise of the caller coroutine that understands the protocol and the IoAwaitable callee. But I don't see any utility or concept that helps the user write such coroutine. The protocol does not define any task or task concept that precisely is the other end of the IoAwaitable requirement. It relies on a "compilation error" if we try to await a IoAwaitable from a coroutine that is not "IoAwaitableProtocol-aware". This is IMHO a weak point of the proposal, as this compilation error type is not good enough in 2026. When you co_await an IoAwaitable inside a non-conforming coroutine, the diagnostic would be some deep "no matching call to await_suspend(coroutine_handle<>)" template bloat, not something like "this coroutine's return type does not satisfy the IoAwaitableProtocol". IoRunnable partially names the coroutine side, but only the launch surface. The goal of the paper is "a small normative protocol that unlocks interoperable tasks." But "interoperable task" is precisely the thing left undefined. I'm not sure if a "IoCoroutine"/"IoPromise" concepts can be precisely defined, but at least we should try to get a more formalized specification. of all the pieces needed to understand and implement the whole IoAwaitableProtocol. I'm pretty sure I don't understand the whole protocol, let me just throw some half-baked proposal just to test if my understanding (and the understanding of other readers like me) is partially correct, overspecified, or underspecified... Problem #1: The language itself does not define an "std::awaiter" concept (which is what the promise must produce from IoAwaitable), if we had one, then we could have the following concepts: +++++++++++++++++ CONCEPT HIERARCHY +++++++++++++++++ Awaiter (the language's awaiter shape: 1-arg await_suspend), maybe we could try to deduce it and add it to the standard ^ IoAwaitable (the protocol operation: 2-arg await_suspend(h, env)) [from the paper] ^ IoPromise (a promise that can be given an env and whose await_transform turns ANY IoAwaitable into an Awaiter) [the missing piece] ^ IoCoroutine (a return type whose promise is an IoPromise) [the missing piece] ^ IoRunnable (an IoCoroutine that is also launchable) +++++++++++++++++ Some sketches: Note: +++++++++++++++++ // A maximally-general IoAwaitable archetype struct io_awaitable_archetype { struct distinct_result {}; bool await_ready() const noexcept { return false; } void await_suspend (std::coroutine_handle<>, const io_env*) const noexcept {} distinct_result await_resume() const noexcept { return {}; } }; static_assert(IoAwaitable<io_awaitable_archetype>); // A promise that can deal with IoAwaitables template <class P> concept IoPromise = // (1) it can be handed an environment. // NORMATIVE PROSE (not checkable here): set_environment(e) shall store e // such that subsequent await_transform calls deliver *e to the // transformed awaitable, and such that any child coroutine constructed // during this coroutine's execution observes e->frame_allocator as the // cached frame allocator. e shall outlive the coroutine. Throws nothing. requires(P& p, const io_env* env) { { p.set_environment(env) } noexcept; } && // (2) it can be told its continuation (called by the awaiting parent on // every // child await, and by a launch function at the root). // NORMATIVE PROSE: set_continuation(c) shall store c such that the // coroutine resumes c at its final suspend point (or noop_coroutine() if // c is null). Throws nothing. requires(P& p) { { p.set_continuation(std::coroutine_handle<>{}) } noexcept; } && // (3) its await_transform turns a protocol awaitable into a language // Awaiter. // NORMATIVE PROSE: await_transform(a) shall yield an Awaiter that, when // the coroutine suspends, invokes a.await_suspend(h, env) with env being // the environment most recently installed via set_environment. requires(P& p, any_io_awaitable a) { { p.await_transform(a) }; } && Awaiter<decltype(std::declval<P&>().await_transform( std::declval<any_io_awaitable>()))> && // (4) it provides a class-scope operator new / operator delete (its own or // inherited), so frame allocation does NOT fall through to the global // ::operator new. This rejects a promise that ignores frame allocation // entirely; note it can only check PRESENCE, not behavior (see prose). // NORMATIVE PROSE (the part the concept CANNOT verify): the coroutine // frame shall be allocated, at the point of allocation, using the memory // resource returned by get_cached_frame_allocator(), and the matching // operator delete shall return that storage to the same resource. A // class-scope operator new that merely forwards to ::operator new // satisfies this concept clause but does NOT satisfy this normative // requirement. requires(std::size_t n, void* ptr) { { P::operator new(n) } -> std::same_as<void*>; { P::operator delete(ptr, n) }; }; template <class T> concept IoCoroutine = requires { typename T::promise_type; } && IoPromise<typename T::promise_type>; template <class T> concept IoRunnable = IoCoroutine<T> && IoAwaitable<T> && requires(T& t, const T& ct, typename T::promise_type& p) { { ct.handle() } noexcept -> std::same_as<std::coroutine_handle<typename T::promise_type>>; { ct.exception() } noexcept -> std::same_as<std::exception_ptr>; { t.release() } noexcept; }; ------------------------------------------------------------ ------------------------------------------------------------ 7) Leaf awaitable vs tasks ------------------------------------------------------------ ------------------------------------------------------------ I find it's hard to specify the requirements for IoAwaitable types that are leaf (NOT a coroutine, no promise type, probably calls the OS) vs an IoAwaitable that is a "task". In the later, the environment must be propagated and I really don't know see how/where. Not sure if that should be part of the protocol, but certainly, if I want to properly implement the whole protocol, I need to know which is the correct time to call p.set_enviroment and p.set_continuation. Isn't that a requirement to fulfill the protocol? ------------------------------------------------------------ ------------------------------------------------------------ 8) Implementation details? ------------------------------------------------------------ ------------------------------------------------------------ Execution_ref shows private parts, shouldn't those be "exposition-only" members or just absent? void const* ex_ = nullptr; detail::executor_vtable const* vt_ = nullptr; ------------------------------------------------------------ ------------------------------------------------------------ 9) execution_conext and executor_ref seem orphan concepts ------------------------------------------------------------ ------------------------------------------------------------ The relationship between those and the rest is confusing. As I understand it, when a leaf IoAwaitable suspends, it picks one of a small set of outcomes — that's its decision. And that's why the executor_ref is needed in the environment. I think it should be clearly stated: "This operation isn't ready → park until the reactor says it's ready." → it registers its continuation with env->executor.context() (the reactor). "This operation is already done / cancelled / synchronously complete → don't park, resume soon, safely." → it calls env->executor.post(cont). "I'm provably on the right thread at a safe point → resume immediately if allowed." → it calls env->executor.dispatch(cont). So the awaitable chooses among park / defer / immediate-transfer. That is a real decision, and it's the awaitable's job because only the awaitable knows the state of its operation. But there is no explanation about the operations provided by executor_ref and execution_context and its service. I think this paper should at least give a non-normative text sayins what those functions do. Finally once you need to go to the execution context (calling "context() on the executor_ref) then isn't the type erasure going away? "use_service" requires a concrete T type that the IoAwaitable must know so aren't we breaking the generic executor promise-land? ------------------------------------------------------------ ------------------------------------------------------------ Summary ------------------------------------------------------------ ------------------------------------------------------------ I know my coroutine knowledge is far from minimal so take all these points with a grain of salt. But if the intention is to have a minimal paper that gives an overview that minimally connects the presented facilities, I think the paper is missing some basic explanations. Best, Ion
participants (9)
-
arnaud.becheler@gmail.com -
Dmitry Arkhipov -
Gennaro Prota -
Ion Gaztañaga -
Mateusz Pusz -
Matt Borland -
Mohammad Nejati -
Ruben Perez -
Vinnie Falco