[capy] Andrzej's feedback
Hi Everyone, I do not feel qualified in the domain of asynchronous I/O to provide a responsible review. But let me share some observations instead. I would like to thank the Capy/Corosio team for developing and sharing these libraries. This is a huge scope, a huge effort, a great lot of moving parts and details to tackle. I am not even sure how this can be reviewed with the level of scrutiny that is required. I appreciate the unprecedented level of the design rationale. The library comes not only with sources, tests and library documentation, but also with lots of documents presenting rationale for every tiny decision. The contribution of this library is not only the code that users can use, but also the accumulated (and shared) knowledge that can be used by anyone, for writing their own I/O framework and avoiding the past mistakes. This alone builds my trust in the authors and the quality of the library. The value proposition of Capy and Corosio is not only in providing a faster and easier to use alternative to ASIO. It is also the responsiveness of the authors and openness for contribution. It gives me confidence that if I encounter some problems with the library in the future, they will be solved in a predictable way. Now, the concerns. A couple of things in the IoAwaitable protocol give discomfort: 1. Having to use thread_local storage 2. Having to use the double-call syntax. 3. Having to use pass they stop_token via await_ready (and therefore risk UB when stop_token is queried before the first suspend). Ideally, these things should not be present in a protocol. However, it seems impossible to provide any alternative design, with coroutines as its basic vehicle, that would have fewer problems like these. 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. One could question one design choice in Capy: why go with coroutines when you have to fight an uphill battle with the language? But then, given that it outperforms ASIO and std::execution, it has clearly earned its place. It surely deserves its place in Boost as "the best you can do with coroutines for I/O". But if I were to consider switching to a different library for async I/O I would also consider/evaluate non-coroutine alternatives. On the usage of error_code and reporting failures. 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. 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. The only model for this that I can adapt is that the contract of stream interaction is similar to the interaction with a human postman: * They can deliver you a parcel * But they can also tell you "I lost your parcel", or "while obtaining the parcel I noticed that the box is half-open", or "I had a car accident, and couldn't deliver your parcel" That is, your success is having had a meeting with the postman and chatted with them and maybe also having obtained the parcel. In that model not receiving a parcel is not an error. This makes sense for the interaction with a single stream, but breaks for the next layer: 1. when you have to interact with two streams at once: pull_from, push_to. 2. when you have "algorithms" such as when_any, when you have to nominate the first successful task. But in the postman model nearly everything is successful. Having read the stream status is success. 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. While the contract of `stream.read_some` is "I promise you nothing else than to inform you what happened", it is not clear to me what the contract of pull_from is. Is it: a) I promise to pump the data from one stream to the other. We will call it a contract violation if I fail. OR b) I will just issue some stream instructions in sequence and tell you what the readouts of controls are. My having told you the readout is already a success; you interpret what this means for you. In this case I do not buy the "ASIO got that right" argument. If ASIO is also unclear about the contract, then ASIO got that wrong also. There are a lot of libraries that are popular, contribute to the success of servers that employ them, but that get their contracts wrong. I am bringing the contract aspect, because I know that the authors of Capy care deeply about this level of clarity. A number of libraries were accepted to Boost while having a muddy contract, and this did not seem to be a show-stopper. My observations are based on reading the documentation and the implementation of some parts of Capy and Corosio, running toy examples and discussing the design aspects with the authors. Disclaimer: I am affiliated with the C++ Alliance, and have occasionally contributed to Capy. Regards, &rzej;
On 7/5/26 1:46 AM, Andrzej Krzemienski via Boost wrote:
Hi Everyone,
....
Regards, &rzej;
Another incredibly insightful review by &rzel !!! Just a couple o observations. &drez observations put their finger on a fundamental problem: C++ doesn't have the right primitive to support this kind of library which is in much demand. When this happens, we often respond by making a library in spite of this. Things can be made to work. But this ultimately unsatisfactory. Having several libraries which have been made to work, hopefully should give a hint what small enhancement to the language is missing to make these kinds of libraries easier to write correctly. This is job of the standards committe aided by our brightest and imaginative minds. Go to it guys! But I think it's long past time for boost to stop focusing in terms of creating libraries that will one day be added to the standard. The standards process, though very useful in the past, but has reached the point where it's a victim of it's success. Its enhancements (save rare cases) complicate the language and library more than they contribute in terms of utility. Many result in a net loss to the future of C++. Boost can/will continue to be a main source of new libraries and try to set the standard from design, coding, testing, review, education, and documentation of C++ libraries. Let the standards committee and the companies who support them do what they will. It's less and less relevant in the future. Robert Ramey
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/M6NXDYD7...
On Sun, Jul 5, 2026 at 12:31 PM Robert Ramey via Boost < boost@lists.boost.org> wrote:
...we often respond by making a library in spite of this. Things can be made to work. But this ultimately unsatisfactory.
I disagree. I have been working with C++ networking and asynchrony now for over a decade. Capy is hardly unsatisfactory. It is fantastic. Because C++20 coroutines are fantastic. No one knows this, because Certain People Who Shall Not Be Named decided that "coroutines are a poor basis for asynchrony" in just 5 paragraphs of text, with no supporting evidence. No benchmarks. No example programs. Presumably, they saw the requirement for the coroutine frame to be allocated as beneath their design sensibilities and decided it was not worth further inquiry. Guess what? I made further inquiry. C++20 coroutines are great. They are comparable to coroutines in other languages, and of course they have their own particular complexities. But this is because C++ offers its own complexities. Other languages don't give you control over how memory is allocated, or give you pointers, and so on.
I think it's long past time for boost to stop focusing in terms of creating libraries that will one day be added to the standard.
This already happened. Look at the recent libraries that have been added to Boost. Capy and Corosio are no exception. They were not designed to be added to the standard. They were designed to replace Boost.Asio, offering a coroutine-native approach. No more, and no less. It is a happy coincidence that they are also worth standardizing. But let's get the ordering in the causal chain correct: the building of the library for its utility in serving our other high level libraries came first. Thanks
I think it's long past time for boost to stop focusing in terms of creating libraries that will one day be added to the standard.
This already happened. Look at the recent libraries that have been added to Boost.
Like mine (OpenMethod). Isn't that the difference between Boost and Beman? J-L
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
On 06/07/2026 15:03, Vinnie Falco via Boost wrote:
On Sun, Jul 5, 2026 at 1:48 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
[...]
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.
Just to note here that this is an important observation and one I'd like to reiterate. "error codes are for errors" is a common misconception in this space. Sometimes in fact an "error" might be on a hot path scenario. This is one reason why reasoning about a separate error channel is often the wrong mental model to adopt. Instead "errors" are really just alternative control flow paths, and as I said, sometimes the hot path. It's one reason why error codes are preferred as they don't force an opinion on the impact, handling, and importantly cost, of a "non-success" path (sometimes expected).
pon., 6 lip 2026 o 17:40 Jamie Allsop via Boost <boost@lists.boost.org> napisał(a):
On 06/07/2026 15:03, Vinnie Falco via Boost wrote:
On Sun, Jul 5, 2026 at 1:48 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
[...]
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.
Just to note here that this is an important observation and one I'd like to reiterate. "error codes are for errors" is a common misconception in this space.
Any idea where this misconception may be coming from?
Sometimes in fact an "error" might be on a hot path scenario. This is one reason why reasoning about a separate error channel is often the wrong mental model to adopt. Instead "errors" are really just alternative control flow paths, and as I said, sometimes the hot path. It's one reason why error codes are preferred as they don't force an opinion on the impact, handling, and importantly cost, of a "non-success" path (sometimes expected).
I do not find it helpful to mix the notion of postcondition failures (this is what I understand errors to be) and being on the hot path. For operations on streams it makes sense to promise nothing, as the implementer is not in control of the environment. But at the level of algorithms like when_any the documentation ( https://develop.capy.cpp.al/capy/reference/boost/capy/when_any-07.html#_desc... ) starts to assign the interpretation to these "just status fields" as being a failure and error. Looks like the library seems confused as to whether to treat non-zero error_codes as errors. And there are ways to handle postcondition violations on hot paths, like https://www.boost.org/doc/libs/latest/libs/outcome/doc/html/tutorial/essenti... Regards, &rzej; _______________________________________________
Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/QR52OMNF...
On Tuesday, July 7th, 2026 at 12:21 AM, Andrzej Krzemienski via Boost <boost@lists.boost.org> wrote:
Just to note here that this is an important observation and one I'd like to reiterate. "error codes are for errors" is a common misconception in this space.
Any idea where this misconception may be coming from?
I think std::error_code provides the right functionality with a poor label and I suspect the label is exactly where the misconception comes from. People see "error" in the type name and assume any non-zero value signals a failure. A better name would be "status_code". There are many times when we want to express the state of something that is neither success nor failure, like peer_not_ready or block_not_linkable, where the program should do something other than either carry on as if all is well or treat it as a failure. I don't think I'm alone in this opinion. There is actually a paper P1028 that describes this idea. I also believe Boost's error_code provides a query to determine if the code actually indicates a failure, which further validates the line of thought.
Steve Gerbino wrote:
I don't think I'm alone in this opinion. There is actually a paper P1028 that describes this idea. I also believe Boost's error_code provides a query to determine if the code actually indicates a failure, which further validates the line of thought.
It does. I also proposed that for std::error_code, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1198r0.html but it wasn't accepted as it would have been an ABI break. Windows COM HRESULT codes and HTTP status codes allow nonzero alternate success.
Apologies for the formatting - I'm fighting with Gmail which does not understand how mailing lists work... On 06/07/2026 23:20, Andrzej Krzemienski wrote:
pon., 6 lip 2026 o 17:40 Jamie Allsop via Boost <boost@lists.boost.org> napisał(a):
On 06/07/2026 15:03, Vinnie Falco via Boost wrote: > On Sun, Jul 5, 2026 at 1:48 AM Andrzej Krzemienski via Boost < > boost@lists.boost.org> wrote: > > [...] > > 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. > > Just to note here that this is an important observation and one I'd like to reiterate. "error codes are for errors" is a common misconception in this space.
Any idea where this misconception may be coming from?
I think we can't shy away from the fact that we named it "error_code" when it probably made sense to go with something like "status_code" or "result_code" though to be fair I think that ship has sailed. This still trips me up when I look at code and see "error_code". It's unfortunate but it is what it is. As I recall (way back) there was some conversation around this but we didn't have the wealth of experience with alternative approaches to actual errors to make it easier to reason about non-error errors without getting drawn into sideways conversations. When we were still hoping to get networking into C++ for 20 there were a lot of crossed conversations about error codes and error channels that were not helped by the naming (and from our discussion solving different problems).
Sometimes in fact an "error" might be on a hot path scenario. This is one reason why reasoning about a separate error channel is often the wrong mental model to adopt. Instead "errors" are really just alternative control flow paths, and as I said, sometimes the hot path. It's one reason why error codes are preferred as they don't force an opinion on the impact, handling, and importantly cost, of a "non-success" path (sometimes expected).
I do not find it helpful to mix the notion of postcondition failures (this is what I understand errors to be) and being on the hot path. For operations on streams it makes sense to promise nothing, as the implementer is not in control of the environment.
But at the level of algorithms like when_any the documentation (https://develop.capy.cpp.al/capy/reference/boost/capy/when_any-07.html#_desc...) starts to assign the interpretation to these "just status fields" as being a failure and error.
I sympathise with this observation and I would say the main issue here is that in many cases error codes might not be an error in the context of postcondition failures (which in past I would have thought often were the purview of exceptions - I can't meet my postconditions so I can't complete...exception). I see that differently as my postcondition is that I can complete, but the result of completion might be that (for example) that I could not read any more bytes from a stream (because there aren't any). This comes back to naming then, and how we reason about this. I think you are right to point out that "when_any" is an example of how what I just said can't really hold in a context where we decide that the notion of a "preferred success", or "expected success outcome" is the criteria to be measured. I'd argue though that that just means that any generalised facility that relies on a simple model which assumes an error code only has one "success" result can only be applied where that assumption is true. If it doesn't then don't use the facility or, perform some kind of translation to achieve that behaviour for your use case allowing the utility to be of value.
Looks like the library seems confused as to whether to treat non-zero error_codes as errors.
And there are ways to handle postcondition violations on hot paths, like https://www.boost.org/doc/libs/latest/libs/outcome/doc/html/tutorial/essenti...
Thanks for sharing this. I've never used it but I'll be sure to familiarise myself with it should I need it in the future. However, you highlight a an important point here and that is that in the cases I am thinking of I'm saying that the error code (poorly named as it is in some contexts) represents, not a violation of postconditions, but rather one of a number of possible outcomes that represents completion of a given operation. One that is to be expected under certain circumstances, and in some cases, possibly very frequent and likely on the hot path. In other words the error code tells you how the operation completes, not if it completed. At some higher level of abstraction we can easily reason that some kinds of completion violates higher level postconditions and that's an error. Which is all to say I think everything you said is pretty spot on but unfortunately error code is probably poorly named and that simply creates confusion. Not all "errors" are failures in a nutshell. Best wishes, Jamie
Regards, &rzej;
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/QR52OMNF...
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
On Mon, Jul 6, 2026 at 2:53 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
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.
Yeah, that's fair. And I think maybe we can include an example or something so there is some review surface.
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?
Well the two-call syntax can be abused. Don't do that.
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.
Okay but that's for implementors. The user doesn't have to worry about that. If thread_local is not available on a particular architecture, the implementation can fall back to the global allocator. Or in more practical terms it simply uses a plain global variable. It turns out that the platforms without thread_local also tend to not have threads (P4182R2 which I can't seem to find at the moment... Mungo?)
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.
We *can* guarantee it. Section 8.3: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf
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).
The user doesn't have to cooperate. The launch function has the burden. P4172 describes three audiences: 1. Application developer 2. Framework author 3. I/O library author The Framework author has the responsibility of making sure that the launch functions and task types they create follow the rules. These authors are fewer in number with a higher level of skill. The expectations are reasonable.
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.
Non-task awaitables do not have an associated coroutine, so they don't have to handle it. They just put their state into the calling coroutines frame, without requiring an allocation. These are written by I/O library authors (group 3 above). Corosio is full of them. read_some_awaitable, and so on.
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.
There is no monolithic "TLS hack." It is not some light switch that is turned on and off. Boost.LEAF and Capy each use their own thread local variables to achieve their ends. Since the variables have different linkages, they are separate from each other and work together Just Fine. Show me the specific interaction where you think these libraries do not compose?
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.
Changing the language should be DIFFICULT and EARNED. Engineers are too hasty to beg the language for features to make their lives easier. Note, we did not go running hat in hand to wg21 saying "please solve this in the language." We especially do not say "we need co_yield to act as a final suspend." Instead, we come up with our own solution which does not require changes to the language. It is not perfect, but it gives 16.3 million potential users of networking a path to use coroutines for composition, instead of callbacks. And then we can get field experience will may LATER justify a change to the language. But only after we have at least 9 years under our belt.
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.
You say "there are others" plural. But only give one example. Maybe it is a legitimate example but the way you phrase it implies a wider scope than exist. Please enumerate actual problems so they can be engaged one by one, and not unfalsifiable generalizations. 3. Having to use pass they stop_token via await_ready (and therefore risk UB
when stop_token is queried before the first suspend).
What? Show the undefined behavior you think is happening, please.
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.
You can imagine all you like but please, show concrete examples. The example you just gave, calling cancel() on "some I/O object" from a different thread, is undefined behavior. ALL I/O objects in Corosio are NOT THREAD SAFE. You cannot access them concurrently. Please show a concrete example of undefined behavior.
I did not mean C++26 executors as the alternative. I was thinking more in the vein of https://github.com/ClickHouse/silk.
Stackful coroutines ("fiber" in silk parlance) are wonderful. And Nat Goodspeed should not have waited over twelve years to get them in the standard. They do not replace stackless coroutines, nor do they compete with them. They are a legitimate tool which solves the same problem with a different set of tradeoffs. C++ needs them, just as much as it needs frame-opaque stackless coroutines. And I would argue the language needs frame visible coroutines as well. This is not contested and I have written a paper saying so: *Benefits of Frame-Visible Coroutines for Senders* https://isocpp.org/files/papers/P4166R0.pdf However, they are unsuited to a large class of I/O problems, networking in particular. Jamie, do you want to build your system on fibers, or stackless coroutines?
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.
That's a false premise. In this case the Right thing and the Standard thing are the same thing: error_code for common outcomes and transient states, and exceptions for exceptional outcomes.
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.
Of course I am serious. And I did not choose the name error_code. It is what it is, and it cannot be changed. That said, this is how it is being used. To argue "you shouldn't use it that way" is not a serious debate when the customs have already been adopted. And I would say, that returning distinguishable, successful outcomes from error_code is fully in scope. Again I point to Boost.System: https://github.com/boostorg/system/blob/bc7c00fa67501ceadfde8e920835502340e8... Why does `bool failed()` exist, if according to you there should only be one value of "success?" Was Peter Dimov wrong?
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.
A quarter-century of established practice. Show me your alternative. Then let's compare. Thanks
On Mon, Jul 6, 2026 at 3:58 PM Vinnie Falco <vinnie.falco@gmail.com> wrote:
It turns out that the platforms without thread_local also tend to not have threads (P4182R2 which I can't seem to find at the moment... Mungo?)
Found it *A Citable Inventory of Platforms, Operating Systems, and Compiler Toolchains* https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4182r1.pdf Thanks
wt., 7 lip 2026 o 00:58 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
On Mon, Jul 6, 2026 at 2:53 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
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.
Yeah, that's fair. And I think maybe we can include an example or something so there is some review surface.
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?
Well the two-call syntax can be abused. Don't do that.
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.
Okay but that's for implementors. The user doesn't have to worry about that. If thread_local is not available on a particular architecture, the implementation can fall back to the global allocator. Or in more practical terms it simply uses a plain global variable. It turns out that the platforms without thread_local also tend to not have threads (P4182R2 which I can't seem to find at the moment... Mungo?)
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.
We *can* guarantee it. Section 8.3:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf
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).
The user doesn't have to cooperate. The launch function has the burden. P4172 describes three audiences:
1. Application developer 2. Framework author 3. I/O library author
The Framework author has the responsibility of making sure that the launch functions and task types they create follow the rules. These authors are fewer in number with a higher level of skill. The expectations are reasonable.
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.
Non-task awaitables do not have an associated coroutine, so they don't have to handle it. They just put their state into the calling coroutines frame, without requiring an allocation. These are written by I/O library authors (group 3 above). Corosio is full of them. read_some_awaitable, and so on.
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.
There is no monolithic "TLS hack." It is not some light switch that is turned on and off. Boost.LEAF and Capy each use their own thread local variables to achieve their ends. Since the variables have different linkages, they are separate from each other and work together Just Fine.
Show me the specific interaction where you think these libraries do not compose?
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.
Changing the language should be DIFFICULT and EARNED. Engineers are too hasty to beg the language for features to make their lives easier. Note, we did not go running hat in hand to wg21 saying "please solve this in the language." We especially do not say "we need co_yield to act as a final suspend."
Instead, we come up with our own solution which does not require changes to the language. It is not perfect, but it gives 16.3 million potential users of networking a path to use coroutines for composition, instead of callbacks. And then we can get field experience will may LATER justify a change to the language. But only after we have at least 9 years under our belt.
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.
You say "there are others" plural. But only give one example. Maybe it is a legitimate example but the way you phrase it implies a wider scope than exist. Please enumerate actual problems so they can be engaged one by one, and not unfalsifiable generalizations.
3. Having to use pass they stop_token via await_ready (and therefore
risk UB when stop_token is queried before the first suspend).
What? Show the undefined behavior you think is happening, please.
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.
You can imagine all you like but please, show concrete examples. The example you just gave, calling cancel() on "some I/O object" from a different thread, is undefined behavior.
ALL I/O objects in Corosio are NOT THREAD SAFE. You cannot access them concurrently.
Please show a concrete example of undefined behavior.
I did not mean C++26 executors as the alternative. I was thinking more in the vein of https://github.com/ClickHouse/silk.
Stackful coroutines ("fiber" in silk parlance) are wonderful. And Nat Goodspeed should not have waited over twelve years to get them in the standard. They do not replace stackless coroutines, nor do they compete with them. They are a legitimate tool which solves the same problem with a different set of tradeoffs.
C++ needs them, just as much as it needs frame-opaque stackless coroutines. And I would argue the language needs frame visible coroutines as well. This is not contested and I have written a paper saying so:
*Benefits of Frame-Visible Coroutines for Senders* https://isocpp.org/files/papers/P4166R0.pdf
However, they are unsuited to a large class of I/O problems, networking in particular.
Jamie, do you want to build your system on fibers, or stackless coroutines?
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.
That's a false premise. In this case the Right thing and the Standard thing are the same thing: error_code for common outcomes and transient states, and exceptions for exceptional outcomes.
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.
Of course I am serious. And I did not choose the name error_code. It is what it is, and it cannot be changed. That said, this is how it is being used. To argue "you shouldn't use it that way" is not a serious debate when the customs have already been adopted. And I would say, that returning distinguishable, successful outcomes from error_code is fully in scope. Again I point to Boost.System:
https://github.com/boostorg/system/blob/bc7c00fa67501ceadfde8e920835502340e8...
Why does `bool failed()` exist, if according to you there should only be one value of "success?" Was Peter Dimov wrong?
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.
A quarter-century of established practice. Show me your alternative. Then let's compare.
I am answering before going to sleep, so this may not be a complete answer that you deserve, but I do not want you to wait another day for the reply. I expressed my discomfort with the double call syntax. I chose word discomfort to convey that I am not opposed to the solution (I wouldn't be able to come up with a better one without violating other goals), but I note that unlike a lot of other features in Boost, we do not get this "easy to use correctly, hard to use incorrectly" effect. For examples of how this can be misused. One has already been provided, I think by Rainer: run_async(exec)(the_job(1, "8080")); And `the_job` may be a factory function or a getter, or a wrapper function. It is generally a reasonable thing to do, but not when the double-call syntax (with TLS under the hood) is in action and the slight misordering of the sight effects may break the system. Another example is when somebody decides to refactor the code in order not to repeat themselves: auto exec_runner = run_async(exec); /// ... run_all_coros(move(exec_runner), {my_job1(), my_job2(), my_job3()}); The argument of "three audiences" has been made a couple of times. I hear that there is going to be a "I/O library author". When writing a custom I/O functionality they may need to provide their own IoAwaitable. I read that such an "author has the responsibility of making sure that the launch functions and task types they create follow the rules". Can you point me to the place where these rules are written down with the level of detail necessary for the "I/O library author" to do their job correctly? The interesting case is when we have an IoAwaitable (to be associated with a coroutine) that does not have the initial suspend and does execute *part* of the coroutine's body before suspending. If in that part of the body (before the first real suspend I -- the ordinary programmer, not I/O provider) get the stop_token with `co_await this_coro`, I will get a null pointer dereference. I enclose an example that demonstrates this. Now, going back to error_code and Boost.System. If we forego the naming issue, the idea behind the error code is that your functions have weak postcondition: everything is a success. Having obtained the status code is success. Then you can inspect it and make the call on what to do next. In this view, the function std::error_category::failed(),mentioned by others would not make sense, as this would have been the decision made by someone else -- not the caller. It makes sense to use error_code at the level of stream operations. I am not sure the same technique makes sense at higher levels. Function when_all definately does not follow the Boost.System model, as it assumes any non-zero code is a failure. Function read_at_least -- using error_code to represent a precondition violation -- might be in the spirit of Boost.System, but this is so confusing: to refuse to communicate what is obviously a bug in the caller. I also think the function normalize_openssl_shutdown_read_error goes against the idea of Boost.System, as I understand it. My understanding has always been that error codes once produced stay as they are (this may be valuable information to whoever processes them at the higher level), and users use different conditions at different application levels to interpret the original code in a way suitable at their level. In contrast, normalize_openssl_shutdown_read_error discards the original value and just produces another. Regards, &rzej;
śr., 8 lip 2026 o 02:19 Andrzej Krzemienski <akrzemi1@gmail.com> napisał(a):
wt., 7 lip 2026 o 00:58 Vinnie Falco <vinnie.falco@gmail.com> napisał(a):
On Mon, Jul 6, 2026 at 2:53 PM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
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.
Yeah, that's fair. And I think maybe we can include an example or something so there is some review surface.
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?
Well the two-call syntax can be abused. Don't do that.
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.
Okay but that's for implementors. The user doesn't have to worry about that. If thread_local is not available on a particular architecture, the implementation can fall back to the global allocator. Or in more practical terms it simply uses a plain global variable. It turns out that the platforms without thread_local also tend to not have threads (P4182R2 which I can't seem to find at the moment... Mungo?)
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.
We *can* guarantee it. Section 8.3:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf
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).
The user doesn't have to cooperate. The launch function has the burden. P4172 describes three audiences:
1. Application developer 2. Framework author 3. I/O library author
The Framework author has the responsibility of making sure that the launch functions and task types they create follow the rules. These authors are fewer in number with a higher level of skill. The expectations are reasonable.
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.
Non-task awaitables do not have an associated coroutine, so they don't have to handle it. They just put their state into the calling coroutines frame, without requiring an allocation. These are written by I/O library authors (group 3 above). Corosio is full of them. read_some_awaitable, and so on.
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.
There is no monolithic "TLS hack." It is not some light switch that is turned on and off. Boost.LEAF and Capy each use their own thread local variables to achieve their ends. Since the variables have different linkages, they are separate from each other and work together Just Fine.
Show me the specific interaction where you think these libraries do not compose?
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.
Changing the language should be DIFFICULT and EARNED. Engineers are too hasty to beg the language for features to make their lives easier. Note, we did not go running hat in hand to wg21 saying "please solve this in the language." We especially do not say "we need co_yield to act as a final suspend."
Instead, we come up with our own solution which does not require changes to the language. It is not perfect, but it gives 16.3 million potential users of networking a path to use coroutines for composition, instead of callbacks. And then we can get field experience will may LATER justify a change to the language. But only after we have at least 9 years under our belt.
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.
You say "there are others" plural. But only give one example. Maybe it is a legitimate example but the way you phrase it implies a wider scope than exist. Please enumerate actual problems so they can be engaged one by one, and not unfalsifiable generalizations.
3. Having to use pass they stop_token via await_ready (and therefore
risk UB when stop_token is queried before the first suspend).
What? Show the undefined behavior you think is happening, please.
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.
You can imagine all you like but please, show concrete examples. The example you just gave, calling cancel() on "some I/O object" from a different thread, is undefined behavior.
ALL I/O objects in Corosio are NOT THREAD SAFE. You cannot access them concurrently.
Please show a concrete example of undefined behavior.
I did not mean C++26 executors as the alternative. I was thinking more in the vein of https://github.com/ClickHouse/silk.
Stackful coroutines ("fiber" in silk parlance) are wonderful. And Nat Goodspeed should not have waited over twelve years to get them in the standard. They do not replace stackless coroutines, nor do they compete with them. They are a legitimate tool which solves the same problem with a different set of tradeoffs.
C++ needs them, just as much as it needs frame-opaque stackless coroutines. And I would argue the language needs frame visible coroutines as well. This is not contested and I have written a paper saying so:
*Benefits of Frame-Visible Coroutines for Senders* https://isocpp.org/files/papers/P4166R0.pdf
However, they are unsuited to a large class of I/O problems, networking in particular.
Jamie, do you want to build your system on fibers, or stackless coroutines?
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.
That's a false premise. In this case the Right thing and the Standard thing are the same thing: error_code for common outcomes and transient states, and exceptions for exceptional outcomes.
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.
Of course I am serious. And I did not choose the name error_code. It is what it is, and it cannot be changed. That said, this is how it is being used. To argue "you shouldn't use it that way" is not a serious debate when the customs have already been adopted. And I would say, that returning distinguishable, successful outcomes from error_code is fully in scope. Again I point to Boost.System:
https://github.com/boostorg/system/blob/bc7c00fa67501ceadfde8e920835502340e8...
Why does `bool failed()` exist, if according to you there should only be one value of "success?" Was Peter Dimov wrong?
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.
A quarter-century of established practice. Show me your alternative. Then let's compare.
I am answering before going to sleep, so this may not be a complete answer that you deserve, but I do not want you to wait another day for the reply.
I expressed my discomfort with the double call syntax. I chose word discomfort to convey that I am not opposed to the solution (I wouldn't be able to come up with a better one without violating other goals), but I note that unlike a lot of other features in Boost, we do not get this "easy to use correctly, hard to use incorrectly" effect. For examples of how this can be misused. One has already been provided, I think by Rainer:
run_async(exec)(the_job(1, "8080"));
And `the_job` may be a factory function or a getter, or a wrapper function. It is generally a reasonable thing to do, but not when the double-call syntax (with TLS under the hood) is in action and the slight misordering of the sight effects may break the system.
Another example is when somebody decides to refactor the code in order not to repeat themselves:
auto exec_runner = run_async(exec); /// ... run_all_coros(move(exec_runner), {my_job1(), my_job2(), my_job3()});
The argument of "three audiences" has been made a couple of times. I hear that there is going to be a "I/O library author". When writing a custom I/O functionality they may need to provide their own IoAwaitable. I read that such an "author has the responsibility of making sure that the launch functions and task types they create follow the rules". Can you point me to the place where these rules are written down with the level of detail necessary for the "I/O library author" to do their job correctly?
The interesting case is when we have an IoAwaitable (to be associated with a coroutine) that does not have the initial suspend and does execute *part* of the coroutine's body before suspending. If in that part of the body (before the first real suspend I -- the ordinary programmer, not I/O provider) get the stop_token with `co_await this_coro`, I will get a null pointer dereference. I enclose an example that demonstrates this.
My apologies. Providing a complicated example in the middle of the night had little chances of succeeding. I sent you something that is not an IoAwaitable. I now enclose an example of an IoAwaitable that gives the impression of being correctly implemented -- could pass a review -- but that triggers UB in some cases. This one is a bit longer. Regards, &rzej;
Now, going back to error_code and Boost.System. If we forego the naming issue, the idea behind the error code is that your functions have weak postcondition: everything is a success. Having obtained the status code is success. Then you can inspect it and make the call on what to do next. In this view, the function std::error_category::failed(),mentioned by others would not make sense, as this would have been the decision made by someone else -- not the caller.
It makes sense to use error_code at the level of stream operations. I am not sure the same technique makes sense at higher levels. Function when_all definately does not follow the Boost.System model, as it assumes any non-zero code is a failure. Function read_at_least -- using error_code to represent a precondition violation -- might be in the spirit of Boost.System, but this is so confusing: to refuse to communicate what is obviously a bug in the caller.
I also think the function normalize_openssl_shutdown_read_error goes against the idea of Boost.System, as I understand it. My understanding has always been that error codes once produced stay as they are (this may be valuable information to whoever processes them at the higher level), and users use different conditions at different application levels to interpret the original code in a way suitable at their level. In contrast, normalize_openssl_shutdown_read_error discards the original value and just produces another.
Regards, &rzej;
On 06/07/2026 23:58, Vinnie Falco via Boost wrote:
On Mon, Jul 6, 2026 at 2:53 PM Andrzej Krzemienski <akrzemi1@gmail.com> > wrote: > [...]
I did not mean C++26 executors as the alternative. I was thinking more >> in the vein of https://github.com/ClickHouse/silk. >> > > Stackful coroutines ("fiber" in silk parlance) are wonderful. And Nat > Goodspeed should not have waited over twelve years to get them in the > standard. They do not replace stackless coroutines, nor do they compete > with them. They are a legitimate tool which solves the same problem with a > different set of tradeoffs. > > C++ needs them, just as much as it needs frame-opaque stackless coroutines. > And I would argue the language needs frame visible coroutines as well. This > is not contested and I have written a paper saying so: > > *Benefits of Frame-Visible Coroutines for Senders* > https://isocpp.org/files/papers/P4166R0.pdf > However, they are unsuited to a large class of I/O problems, networking in > particular. > > Jamie, do you want to build your system on fibers, or stackless coroutines? > I wish I could use a thread-per-connection, on a system with latency-free context switching and unlimited memory :-p
Seriously though, while I know some people are doing a great job of advancing the state of the art for stackful coroutines (and therefore fibers) they're not as good a fit for the kind of scalability we need. Stackless coroutines are that building block. Pre std::coroutine and Capy we made use of Asio stackless coroutines for the parts of the code that really needed it: https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/compositi... Jamie
participants (7)
-
Andrzej Krzemienski -
Jamie Allsop -
Jean-Louis Leroy -
Peter Dimov -
Robert Ramey -
Steve Gerbino -
Vinnie Falco