pon., 6 lip 2026 o 16:03 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
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
Slightly as a nit-pick, I have just learnt that you also have to study the not-proposed Boost.HTTP to be able to review the concept BufferSing, as it has no real model in Capy or Corosio.
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?
Why does the doc page describing the launching of coroutines ( https://develop.capy.cpp.al/capy/4.coroutines/4b.launching.html) have a red warning box? Some people do not like thread_local based on its implementation difficulties: requires dynamic memory allocation (and no custom allocator can be provided), is not implementable on some architectures. My discomfort is not about the feature itself, but how it is used. The usage is based on the assumption that the read from the variable happens (1) immediately after the write (no intervening accesses) and (2) on the same thread. And since you cannot enforce that, you have to trust the user, and maybe even trust your luck. But because this hack is made to move the complexity out of the user's sight, the user will not cooperate (they do not know that they have to). I am not even sure how this can be implemented for non-task IoAwaitables when in a thread executing the coroutine may be different than the thread resuming the coroutine, in a thread pool executor. Also, as with any other hacks, they work under the assumption that no-one else is doing their own hacks at the same time. Boost.LEAF uses the TLS hack, and it only works until you have to interoperate with a coroutine library.
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.
In fact, I wish that the problem of environment propagation were addressed by a dedicated language feature that indicates the intention explicitly and allows the compiler to verify the correct usage. My discomfort is not about the strangeness of the notation but about how easy it is to misuse it. The red warning box in the docs confirms this. BTW, the example in the warning box is wrong. That code would not compile (and rightly so). But there are others that would compile and silently get a different behavior.
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.
I imagine a coroutine where a part of its body is executed immediately, without suspension. I can imagine a stop being requested from a different thread executing a different continuation. Say, calling .cancel() on some IO object.
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.
This may be so.
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:
I did not mean C++26 executors as the alternative. I was thinking more in the vein of https://github.com/ClickHouse/silk.
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?
I am in favour of doing the right thing rather than doing what the Standard does. They can of course coincide, but the guidance from the standard alone is insufficient motivation.
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.
I am not sure if you are serious. I indeed attach semantic significance to the type name. Why did you call the type providing access to a UDP socket in Corosio `udp_socket`? You could have called it `sun_flower` and then insist that people should not attach semantic significance to type names.
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.
My issue is not the documentation of individual functions, but with the general philosophy of handling disappointments.
There is nothing wrong with error_code returning something which does not indicate an "error" in the strict sense.
Oh, there is *something* something wrong with this: the misleading naming. But you already explained you are not fond of attaching semantic significance to names.
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.
The Capy's documentation seems to have a different point of view than yours. It just calls non-default ec values errors: https://develop.capy.cpp.al/capy/reference/boost/capy/when_any-07.html#_desc... .
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
"errors"? Looks like you attached a semantic significance to the type name "error_code".
and no way to distinguish the stream) don't generalize to the other types provided by the library.
Sometimes the docs say error_code is part of the stream interface. But sometimes the error_code is used not in connection to any stream, for instance to signal precondition violations: https://github.com/cppalliance/capy/blob/develop-2/include/boost/capy/read_a...
In this case I do not buy the "ASIO got that right" argument.
Asio doesn't have any dual-stream interfaces.
ASIO uses type error_code to signal non-errors. Regards, &rzej;
Thanks