On Sun, Jul 5, 2026 at 1:48 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
I would like to thank the Capy/Corosio team for developing and sharing these libraries.
Thanks!
I am not even sure how this can be reviewed with the level of scrutiny that is required.
That's easy! 1. Learn coroutines 2. Write programs 3. Read the Corosio and Capy documentation 4. Study the interfaces 5. Run some experiments 6. Write the review
A couple of things in the IoAwaitable protocol give discomfort:
1. Having to use thread_local storage
WG21 offers alternatives: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p3980r1.html And what is wrong with thread local? Why do we have these features in the language if we aren't going to use them?
2. Having to use the double-call syntax.
What's wrong with it? We have std::ranges with its pipelines. We have std::execution now which stretches the syntax again. Double-call syntax is a practical solution, and it doesn't require changing the language. Compare this to Reflection. Or Contracts.
3. Having to use pass they stop_token via await_ready (and therefore risk UB when stop_token is queried before the first suspend).
await_ready is an optimization. If the value is available immediately, then return "true" from await_ready and then return the co_await value from await_resume. There's no operation to stop. If you have an operation that is stoppable, you return false from await_ready. Where I think you are going is the idea of a synchronous operation. Yet synchronous operations are by definition not cancelable. If the caller is blocked on a synchronous operation, how could they issue the stop command? It doesn't make sense.
What can we conclude from this? It looks like C++ coroutines are not well suited for I/O-like flows with environment propagation. You can do it *somehow*: just pass all you need in the coroutine's function parameter list, but it is not ergonomic or type-safe. I think the review of Capy may be exposing a design flaw in coroutines. If the goal of Capy is to provide the best third-party library asynchronous-I/O coroutine implementation, then the current design may indeed be the best (as in "no better design can be thought of"). But if the goal is to aim at standardization, I do not think that such unsafe design would work. The action would rather be, "fix coroutines in the language".
This makes me think that coroutines in C++ may be a dead end. This would be consistent with some other observations: scarce adoption in the community and WG21 itself choosing to use a different model for asynchronous flows.
I disagree. What I think instead, is that there is a general and consistent pattern where the committee turns its nose up at practical working solutions and instead favors grandiose intellectual frameworks. Capy and Corosio are pedestrian. Networking and byte-oriented I/O are unsexy. The glory is in designing the "Stepanov-scale asynchronous abstraction." There is no excitement in "amortizing the cost of coroutine frame storage" or "ambient propagation of coroutine frame allocator." Well, I find it exciting since it serves users, but I am an outlier.
One could question one design choice in Capy: why go with coroutines...if I were to consider switching to a different library for async I/O I would also consider/evaluate non-coroutine alternatives.
Because you can't beat the syntax. Look at Section 14.2 here. The first code block is what is currently shipping in C++26. the second code block is your "dead end" coroutines: https://isocpp.org/files/papers/P4014R2.pdf The library doesn't give me a clear picture on how it handles and how I
should handle the non-clear-sky situations. For things that represent obvious disappointments -- broken connection, problems on the wire, not enough memory to process a request -- it sometimes returns an error_code and sometimes throws an exception. It looks like the division is: * for things happening on the wire, or in the system's native part of the I/O implementation, use error_code * for problems with resources in the implementation of the library throw exceptions.
Right, and isn't this the guidance from the Standard? But then also, the library returns error_codes for situations that are not
errors by any measure: for situations that I desire: * Successfully receiving full stream content. * Stopping the running process upon my request.
Who said they are supposed to be "errors"? `error_code` is just a label. You are attaching semantic significance to the type name. The contract of the algorithm is preeminent. And every algorithm in Capy and Corosio has documentation on what to expect. If something is missing, please open an issue - you know we will attend to it. There is nothing wrong with error_code returning something which does not indicate an "error" in the strict sense. boost::error_code even has a mechanism to broaden the API past what the Standard offers, to know which codes are failures and which are not.
In the case of push_to/pull_from, you might be interested in knowing which of the two streams was the cause of disappointment. But the interface does not let you get this information. Interaction with two postmen would require two statuses.
This could be a problem, yes. I don't think it generalizes though. That is, that the specific issue of these algorithms (two streams which can produce errors and no way to distinguish the stream) don't generalize to the other types provided by the library.
In this case I do not buy the "ASIO got that right" argument.
Asio doesn't have any dual-stream interfaces. Thanks