Hi all, As you might know, I'm currently using Capy and Corosio in 3 projects: * Boost.Redis [1]: I've implemented a Corosio API matching what we had for Asio. Currently in a PR that will be merged if the libraries are accepted. * A fork of Boost.MySQL [2]. It is functional, but unclear if I will propose it as a new library, or add it to the existing one. * A PostgreSQL library that I'm writing [3]. I'm in general happy. Coroutines make everything much simpler. I don't have this "everything wants to eat me" feeling that writing Asio universal async operations transmits. Benchmarks are also quite promising, well above what we were able to get with Asio. Marcelo did the benchmarks, so I'll leave him to tell the whole story here. I've got some real questions that need answers before emitting a review. 1) What's the status of the interoperability with Asio, and what can we expect? Asio currently has many users who won't migrate their existing code from one day to another. There is a pull request in Capy [4] addressing this. I'd like to know what to expect. Especially, how seamless would the interaction with Corosio (rather than Capy) be? Would the following work? // Code using capy/corosio that creates I/O objects capy::task<> my_capy_task() { corosio::tcp_socket sock {co_await capy::this_coro::executor}; co_await sock.connect(/* whatever */); } // Represents the main application, still using Asio. // At some point, it needs to call out to code using capy/corosio asio::awaitable<void> co_main() { auto exec = capy::wrap_asio_executor(co_await asio::this_coro::executor); co_await capy::asio_spawn(my_capy_task(), compute()); } int main() { asio::io_context ctx; asio::co_spawn(ctx, co_main(), asio::detached); ctx.run(); } In real use cases, it'd be libraries (like Boost.Redis) that may create I/O objects on behalf of the user. 2) What's the rationale for having tls_context::set_hostname but no tls_stream::set_hostname? Say I have a tls_stream I want to re-use for connecting to different hosts (e.g. HTTP redirection, or Redis Sentinel). Different hosts should have different SNIs. But IIUC tls_context is supposed to be set up beforehand and not modified after the stream is created. How do I do this with Corosio? Note that I raised [5] some time ago - this may be a good time to discuss it (I understand everyone has been busy, don't take this as a criticism). 3) Asio has the concept of error dispositions [6]. In a nutshell, these are a generalization of error codes, so custom error types can be used in error code-aware functionality, like when_any. This is useful in code using specialized types. Future candidate Boost.Http uses this in its router [8], and I intended to use it as a way to attach error messages in my database libraries. I discuss my use case in more detail in this issue [9]. Have you considered the feature? 4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled? 5) I initially found the when_any semantics confusing. It uses the equivalent to Asio's wait_for_one_success semantics [10]. Asio has also wait_for_one, and it turns out it's the one I tend to need more. Docs [11] propose a workaround - but io_task<std::error_code> doesn't look that good. Is there a chance of adding first-class support for this? What is the rationale behind making wait_for_one_success semantics the default? 6) Can I write types that satisfy ConstBufferSequence or DynamicBuffer without explicitly depending on Capy? This would be useful for low-level protocol libraries that want to take as few dependencies as possible. Setting the concrete const_buffer and mutable_buffer types apart, the usual way of representing "raw bytes" would be span<const unsigned char>/span<std::byte>. For instance, std::as_bytes [12] uses std::byte. If I got the reasoning behind ConstBufferSequence right [13], the purpose is to avoid allocations with vectored I/O. I'd say it's reasonable to allow std::array<std::span<const unsigned char>> to satisfy ConstBufferSequence. Note that I have read The Span Reflex [14]. I agree with the need of ConstBufferSequence. While I personally don't agree with the need of the concrete const_buffer and mutable_buffer types, I understand the rationale of avoiding std::byte. I'm not challenging these types, only asking for better compatibility with std::span. I'll probably get more questions as I explore the parts of the library that I don't have implementation experience with. Congratulations on the work, it's really great. Regards, Ruben. [1] https://github.com/boostorg/redis/pull/417 [2] https://github.com/anarthal/mycosql [3] https://github.com/anarthal/nativepg [4] https://github.com/cppalliance/capy/pull/246 [5] https://github.com/cppalliance/corosio/issues/239 [6] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/Disposit... [7] https://github.com/cppalliance/capy/issues/298 [8] https://github.com/cppalliance/http/blob/1ab0b6fe1763618215d72baaca4859b9a47... [9] https://github.com/cppalliance/capy/issues/298 [10] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/overview/compositi... [11] https://master.capy.cpp.al/capy/4.coroutines/4f.composition.html#_errors_do_... [12] https://en.cppreference.com/cpp/container/span/as_bytes [13] https://master.capy.cpp.al/capy/5.buffers/5a.overview.html#_the_concept_driv... [14] https://www.vinniefalco.com/p/the-span-reflex-when-concrete-thinking
On Wed, Jun 24, 2026 at 12:05 AM Ruben Perez via Boost < boost@lists.boost.org> wrote:
Hi all,
As you might know, I'm currently using Capy and Corosio in 3 projects:
* Boost.Redis [1]: I've implemented a Corosio API matching what we had for Asio. Currently in a PR that will be merged if the libraries are accepted. * A fork of Boost.MySQL [2]. It is functional, but unclear if I will propose it as a new library, or add it to the existing one. * A PostgreSQL library that I'm writing [3].
I'm in general happy. Coroutines make everything much simpler. I don't have this "everything wants to eat me" feeling that writing Asio universal async operations transmits. Benchmarks are also quite promising, well above what we were able to get with Asio. Marcelo did the benchmarks, so I'll leave him to tell the whole story here.
I've got some real questions that need answers before emitting a review.
1) What's the status of the interoperability with Asio, and what can we expect? Asio currently has many users who won't migrate their existing code from one day to another.
There is a pull request in Capy [4] addressing this. I'd like to know what to expect. Especially, how seamless would the interaction with Corosio (rather than Capy) be? Would the following work?
// Code using capy/corosio that creates I/O objects capy::task<> my_capy_task() { corosio::tcp_socket sock {co_await capy::this_coro::executor}; co_await sock.connect(/* whatever */); }
// Represents the main application, still using Asio. // At some point, it needs to call out to code using capy/corosio asio::awaitable<void> co_main() { auto exec = capy::wrap_asio_executor(co_await asio::this_coro::executor); co_await capy::asio_spawn(my_capy_task(), compute()); }
int main() { asio::io_context ctx; asio::co_spawn(ctx, co_main(), asio::detached); ctx.run(); }
I am not sure what `compute()` is in this example, but generally yes. You can also wrap asio's stream so they are a capy::Stream and vice versa. You can find the docs for the PR here: https://246.capy.prtest3.cppalliance.org/capy/4.coroutines/4i.asio-integrati... I was hoping we could get this merged by the review, but the review announcement snuck up on me, so it didn't get finished in time.
I am not sure what `compute()` is in this example, but generally yes.
I completely messed the line up, sorry. It should have read asio::awaitable<void> co_main() { auto exec = capy::wrap_asio_executor(co_await asio::this_coro::executor); co_await capy::asio_spawn(exec, my_capy_task()); } Out of curiosity, how does the out-of-the-box Corosio integration work? I mean, the example uses asio::io_context::run(). How does this get to service Corosio's reactor?
On Wed, Jun 24, 2026 at 12:40 AM Ruben Perez <rubenperez038@gmail.com> wrote:
I am not sure what `compute()` is in this example, but generally yes.
I completely messed the line up, sorry. It should have read
asio::awaitable<void> co_main() { auto exec = capy::wrap_asio_executor(co_await asio::this_coro::executor); co_await capy::asio_spawn(exec, my_capy_task()); }
Out of curiosity, how does the out-of-the-box Corosio integration work? I mean, the example uses asio::io_context::run(). How does this get to service Corosio's reactor?
You create a service through the asio executor that contains a capy::execution_context - and vice versa. When the corosio::socket then gets the scheduler from this context, the scheduler *should* create a thread for the reactor. Then, the completion would do a dispatch back onto the asio executor. The above isn't clearly defined in capy yet, which is another reason it's not merged at this point.
Out of curiosity, how does the out-of-the-box Corosio integration work? I mean, the example uses asio::io_context::run(). How does this get to service Corosio's reactor?
You create a service through the asio executor that contains a capy::execution_context - and vice versa. When the corosio::socket then gets the scheduler from this context, the scheduler *should* create a thread for the reactor. Then, the completion would do a dispatch back onto the asio executor.
The above isn't clearly defined in capy yet, which is another reason it's not merged at this point.
Thanks. I thought io_context::run() used the calling thread to run the scheduler. If I have the time, I'll try to experiment with this before the review ends. Best, Ruben.
asio::awaitable<void> co_main() { auto exec = capy::wrap_asio_executor(co_await asio::this_coro::executor); co_await capy::asio_spawn(exec, my_capy_task()); }
Out of curiosity, how does the out-of-the-box Corosio integration work? I mean, the example uses asio::io_context::run(). How does this get to service Corosio's reactor?
You create a service through the asio executor that contains a capy::execution_context - and vice versa. When the corosio::socket then gets the scheduler from this context, the scheduler *should* create a thread for the reactor. Then, the completion would do a dispatch back onto the asio executor.
The above isn't clearly defined in capy yet, which is another reason it's not merged at this point.
I've tried the following and it fails: capy::task<void> capy_work(int i) { std::cout << "Capy task started\n"; corosio::tcp_socket sock{co_await capy::this_coro::executor}; auto [ec] = co_await sock.connect(corosio::endpoint("127.0.0.1:8000")); std::cout << "Capy task: " << i << ": " << ec << std::endl; co_return; } net::awaitable<void> co_main() { std::cout << "co_main\n"; auto exec = capy::wrap_asio_executor(co_await net::this_coro::executor); co_await capy::asio_spawn(exec, capy_work(42)); std::cout << "co_main finished\n"; } int main() { net::io_context ctx; net::co_spawn(ctx, co_main(), [](std::exception_ptr exc) { if (exc) std::rethrow_exception(exc); }); ctx.run(); } With the following output: co_main Capy task started terminate called after throwing an instance of 'std::logic_error' what(): io_object::create_handle: service not installed Aborted (core dumped) Looking at the code, corosio I/O objects don't create any services by default, but require these to be already installed in the execution context that you passed. This happens in corosio::io_context constructor. This means that this also fails: capy::task<void> co_main() { std::cout << "Capy task started\n"; corosio::tcp_socket sock{co_await capy::this_coro::executor}; auto [ec] = co_await sock.connect(corosio::endpoint("127.0.0.1:8000")); std::cout << "Capy task finished: " << ec << std::endl; co_return; } int main() { capy::thread_pool ctx{4}; capy::run_async( ctx.get_executor(), []() { std::cout << "Done\n"; }, [](std::exception_ptr exc) { try { std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; } exit(1); })(co_main()); ctx.join(); } Which makes me think that Corosio can only be used with corosio::io_context, and no other execution contexts.
On Thursday, June 25th, 2026 at 12:58 PM, Ruben Perez via Boost <boost@lists.boost.org> wrote:
asio::awaitable<void> co_main() { auto exec = capy::wrap_asio_executor(co_await asio::this_coro::executor); co_await capy::asio_spawn(exec, my_capy_task()); }
Out of curiosity, how does the out-of-the-box Corosio integration work? I mean, the example uses asio::io_context::run(). How does this get to service Corosio's reactor?
You create a service through the asio executor that contains a capy::execution_context - and vice versa. When the corosio::socket then gets the scheduler from this context, the scheduler *should* create a thread for the reactor. Then, the completion would do a dispatch back onto the asio executor.
The above isn't clearly defined in capy yet, which is another reason it's not merged at this point.
I've tried the following and it fails:
capy::task<void> capy_work(int i) { std::cout << "Capy task started\n"; corosio::tcp_socket sock{co_await capy::this_coro::executor}; auto [ec] = co_await sock.connect(corosio::endpoint("127.0.0.1:8000")); std::cout << "Capy task: " << i << ": " << ec << std::endl; co_return; }
net::awaitable<void> co_main() { std::cout << "co_main\n"; auto exec = capy::wrap_asio_executor(co_await net::this_coro::executor); co_await capy::asio_spawn(exec, capy_work(42)); std::cout << "co_main finished\n"; }
int main() { net::io_context ctx;
net::co_spawn(ctx, co_main(), [](std::exception_ptr exc) { if (exc) std::rethrow_exception(exc); });
ctx.run(); }
With the following output:
co_main Capy task started terminate called after throwing an instance of 'std::logic_error' what(): io_object::create_handle: service not installed Aborted (core dumped)
Looking at the code, corosio I/O objects don't create any services by default, but require these to be already installed in the execution context that you passed. This happens in corosio::io_context constructor.
This means that this also fails:
capy::task<void> co_main() { std::cout << "Capy task started\n"; corosio::tcp_socket sock{co_await capy::this_coro::executor}; auto [ec] = co_await sock.connect(corosio::endpoint("127.0.0.1:8000")); std::cout << "Capy task finished: " << ec << std::endl; co_return; }
int main() { capy::thread_pool ctx{4};
capy::run_async( ctx.get_executor(), []() { std::cout << "Done\n"; }, [](std::exception_ptr exc) { try { std::rethrow_exception(exc); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; } exit(1); })(co_main());
ctx.join(); }
Which makes me think that Corosio can only be used with corosio::io_context, and no other execution contexts.
That is correct, for now. The Boost.Asio compatibility is not part of Capy *yet*. It is un-reviewed and not merged. To support this we simply lazily load services.
Which makes me think that Corosio can only be used with corosio::io_context, and no other execution contexts.
That is correct, for now. The Boost.Asio compatibility is not part of Capy *yet*. It is un-reviewed and not merged. To support this we simply lazily load services.
If I'm reading Corosio's code correctly, it is not enough to construct the services, but you also need to run the scheduler. This happens in io_context::run(). How would this work for capy::thread_pool or the Asio integration case?
On Thursday, June 25th, 2026 at 6:06 PM, Ruben Perez via Boost <boost@lists.boost.org> wrote:
Which makes me think that Corosio can only be used with corosio::io_context, and no other execution contexts.
That is correct, for now. The Boost.Asio compatibility is not part of Capy *yet*. It is un-reviewed and not merged. To support this we simply lazily load services.
If I'm reading Corosio's code correctly, it is not enough to construct the services, but you also need to run the scheduler. This happens in io_context::run(). How would this work for capy::thread_pool or the Asio integration case?
This is true. It should not work with capy::thread_pool. Something like tcp_acceptor does need the reactor. The Boost.Asio integration is outside of the scope of the Capy/Corosio review (it is not in develop or master). It is incomplete work.
If I'm reading Corosio's code correctly, it is not enough to construct the services, but you also need to run the scheduler. This happens in io_context::run(). How would this work for capy::thread_pool or the Asio integration case?
This is true. It should not work with capy::thread_pool. Something like tcp_acceptor does need the reactor. The Boost.Asio integration is outside of the scope of the Capy/Corosio review (it is not in develop or master). It is incomplete work.
This needs to go in a red banner at the top of Corosio's documentation. Also, asking whether a feature X that has been brought before is implementable with the current library architecture is completely in scope of a Boost review. Thanks, Ruben.
On Fri, Jun 26, 2026 at 12:24 AM Steve Gerbino via Boost < boost@lists.boost.org> wrote:
On Thursday, June 25th, 2026 at 6:06 PM, Ruben Perez via Boost < boost@lists.boost.org> wrote:
Which makes me think that Corosio can only be used with corosio::io_context, and no other execution contexts.
That is correct, for now. The Boost.Asio compatibility is not part of Capy *yet*. It is un-reviewed and not merged. To support this we simply lazily load services.
If I'm reading Corosio's code correctly, it is not enough to construct the services, but you also need to run the scheduler. This happens in io_context::run(). How would this work for capy::thread_pool or the Asio integration case?
This is true. It should not work with capy::thread_pool. Something like tcp_acceptor does need the reactor. The Boost.Asio integration is outside of the scope of the Capy/Corosio review (it is not in develop or master). It is incomplete work.
So I can only use `corosio::io_context` for the `tcp_acceptor`? Could I create my own substitute?
On Thu, Jun 25, 2026 at 9:07 AM Ruben Perez via Boost <boost@lists.boost.org> wrote:
If I'm reading Corosio's code correctly, it is not enough to construct the services, but you also need to run the scheduler. This happens in io_context::run(). How would this work for capy::thread_pool or the Asio integration case?
You can only use Corosio's reactor with Corosio's io_context. You cannot install Corosio's reactor in other execution contexts, and have them work. Thanks
On Tue, Jun 23, 2026 at 9:05 AM Ruben Perez via Boost <boost@lists.boost.org> wrote:
6) Can I write types that satisfy ConstBufferSequence or DynamicBuffer without explicitly depending on Capy?
In theory, this is possible. Buffer sequences are defined in terms of a range whose value type is convertible to const_buffer or mutable_buffer. This means Capy could add constructors to const_buffer and mutable_buffer for span, or SpanLike, and then it would work. I have refrained from doing so until we get field experience through the asio bridges. It would be nice to keep the buffer sequences compatible with Asio. Although that might require tradeoffs that cost too much. Thanks
On Tue, 23 Jun 2026 at 19:19, Vinnie Falco <vinnie.falco@gmail.com> wrote:
On Tue, Jun 23, 2026 at 9:05 AM Ruben Perez via Boost <boost@lists.boost.org> wrote:
6) Can I write types that satisfy ConstBufferSequence or DynamicBuffer without explicitly depending on Capy?
In theory, this is possible. Buffer sequences are defined in terms of a range whose value type is convertible to const_buffer or mutable_buffer. This means Capy could add constructors to const_buffer and mutable_buffer for span, or SpanLike, and then it would work.
This would suit my use case and IMO reduce friction a lot.
I have refrained from doing so until we get field experience through the asio bridges. It would be nice to keep the buffer sequences compatible with Asio. Although that might require tradeoffs that cost too much.
I understand your posture. I'll try to fill a more complete use case before the review ends. Best, Ruben.
On Tuesday, June 23rd, 2026 at 6:04 PM, Ruben Perez via Boost <boost@lists.boost.org> wrote:
3) Asio has the concept of error dispositions [6]. In a nutshell, these are a generalization of error codes, so custom error types can be used in error code-aware functionality, like when_any. This is useful in code using specialized types. Future candidate Boost.Http uses this in its router [8], and I intended to use it as a way to attach error messages in my database libraries. I discuss my use case in more detail in this issue [9]. Have you considered the feature?
We just haven't gotten to this issue yet. It seems like something we can easily add.
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
5) I initially found the when_any semantics confusing. It uses the equivalent to Asio's wait_for_one_success semantics [10]. Asio has also wait_for_one, and it turns out it's the one I tend to need more. Docs [11] propose a workaround - but io_task<std::error_code> doesn't look that good. Is there a chance of adding first-class support for this? What is the rationale behind making wait_for_one_success semantics the default?
We got some early feedback that pointed in the direction that wait_for_one_success would be more widely applicable. There's nothing preventing us from adding wait_for_one as a first class API and it seems reasonable to me.
3) Asio has the concept of error dispositions [6]. In a nutshell, these are a generalization of error codes, so custom error types can be used in error code-aware functionality, like when_any. This is useful in code using specialized types. Future candidate Boost.Http uses this in its router [8], and I intended to use it as a way to attach error messages in my database libraries. I discuss my use case in more detail in this issue [9]. Have you considered the feature?
We just haven't gotten to this issue yet. It seems like something we can easily add.
Thanks.
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
5) I initially found the when_any semantics confusing. It uses the equivalent to Asio's wait_for_one_success semantics [10]. Asio has also wait_for_one, and it turns out it's the one I tend to need more. Docs [11] propose a workaround - but io_task<std::error_code> doesn't look that good. Is there a chance of adding first-class support for this? What is the rationale behind making wait_for_one_success semantics the default?
We got some early feedback that pointed in the direction that wait_for_one_success would be more widely applicable. There's nothing preventing us from adding wait_for_one as a first class API and it seems reasonable to me.
It'd help. Thanks.
On Tuesday, June 23rd, 2026 at 7:48 PM, Ruben Perez <rubenperez038@gmail.com> wrote:
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
We have a convention throughout the project to always compare against error conditions. Making a change like that would make it the oddball.
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
We have a convention throughout the project to always compare against error conditions. Making a change like that would make it the oddball.
Why? std::errc is an error condition enum, not an error code enum.
On Tuesday, June 23rd, 2026 at 8:08 PM, Ruben Perez via Boost <boost@lists.boost.org> wrote:
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
We have a convention throughout the project to always compare against error conditions. Making a change like that would make it the oddball.
Why? std::errc is an error condition enum, not an error code enum.
To clarify, I mean our error conditions as indicated here: https://develop.capy.cpp.al/capy/reference/boost/capy/cond.html "These are the conditions callers should compare against when handling errors from capy operations."
Steve Gerbino wrote:
On Tuesday, June 23rd, 2026 at 8:08 PM, Ruben Perez via Boost <boost@lists.boost.org> wrote:
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
We have a convention throughout the project to always compare against error conditions. Making a change like that would make it the oddball.
Why? std::errc is an error condition enum, not an error code enum.
To clarify, I mean our error conditions as indicated here: https://develop.capy.cpp.al/capy/reference/boost/capy/cond.html
"These are the conditions callers should compare against when handling errors from capy operations."
Even if you keep capy::cond::canceled, you should still make sure that comparing against std::errc::operation_canceled works (compares equal whenever comparing to cond::cancelled returns true.) And this also applies to any other conditions that have standard equivalents.
On Wednesday, June 24th, 2026 at 10:13 AM, Peter Dimov via Boost <boost@lists.boost.org> wrote:
Steve Gerbino wrote:
On Tuesday, June 23rd, 2026 at 8:08 PM, Ruben Perez via Boost <boost@lists.boost.org> wrote:
> 4) Does capy::cond::canceled add anything vs. just using > std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
We have a convention throughout the project to always compare against error conditions. Making a change like that would make it the oddball.
Why? std::errc is an error condition enum, not an error code enum.
To clarify, I mean our error conditions as indicated here: https://develop.capy.cpp.al/capy/reference/boost/capy/cond.html
"These are the conditions callers should compare against when handling errors from capy operations."
Even if you keep capy::cond::canceled, you should still make sure that comparing against std::errc::operation_canceled works (compares equal whenever comparing to cond::cancelled returns true.)
And this also applies to any other conditions that have standard equivalents.
That is how it currently works, it'll be codified in a regression test.
Ruben Perez wrote:
4) Does capy::cond::canceled add anything vs. just using std::errc::operation_canceled?
No, it doesn't. It is there for consistency with our error types that don't have std spellings.
Would it make sense to just use the errc one and remove capy's? Maybe Peter can suggest something here?
It's usually better to use the standard condition when it exists, yes. That's because foreign error codes usually know how to compare equal against the standard one, but not against yours.
On Tue, Jun 23, 2026 at 10:39 AM Steve Gerbino via Boost < boost@lists.boost.org> wrote:
On Tuesday, June 23rd, 2026 at 6:04 PM, Ruben Perez via Boost < boost@lists.boost.org> wrote:
3) Asio has the concept of error dispositions [6]. ... We just haven't gotten to this issue yet. It seems like something we can easily add.
Error codes and error conditions are one of the most well-designed aspects of C++. I am extremely skeptical of the need for these "dispositions." Thanks
wt., 23 cze 2026 o 18:03 Ruben Perez via Boost <boost@lists.boost.org> napisał(a):
3) Asio has the concept of error dispositions [6]. In a nutshell, these are a generalization of error codes, so custom error types can be used in error code-aware functionality, like when_any. This is useful in code using specialized types. Future candidate Boost.Http uses this in its router [8], and I intended to use it as a way to attach error messages in my database libraries. I discuss my use case in more detail in this issue [9]. Have you considered the feature?
[6]
https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/Disposit...
Would you be able to provide an example illustrating how you would use this? Regards, &rzej;
3) Asio has the concept of error dispositions [6]. In a nutshell, these are a generalization of error codes, so custom error types can be used in error code-aware functionality, like when_any. This is useful in code using specialized types. Future candidate Boost.Http uses this in its router [8], and I intended to use it as a way to attach error messages in my database libraries. I discuss my use case in more detail in this issue [9]. Have you considered the feature?
[6] https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/Disposit...
Would you be able to provide an example illustrating how you would use this?
My use case is including a diagnostic string returned by the server in the error type. For instance: struct extended_error { std::error_code code; std::string diag; }; // complete type in my postgres library here [1] Then you can define your Asio function like this: class connection { public: template <asio::completion_token_for<void(extended_error)> CompletionToken> auto async_connect(const connect_params& params, CompletionToken&& token); }; // complete type here [2] With Capy, I currently have: class connection { public: // *diag will be populated in case of server error boost::capy::io_task<> connect(connect_params params, std::string* diag = nullptr); }; Before anyone asks, I can go down the path of "only network errors are failures" and write: // Succeeds if the server rejects the connection - populates server_errors in this case boost::capy::io_task<> connect(connect_params params, extended_error& server_errors); This is adequate for low-level functionality, but becomes hostile as you build higher level functionality. For now, I've encoded the SQLSTATE returned by the server as a std::error_code, so the user gets _some_ diagnostics even if they didn't use the output parameter [3]. Because extended_error is no longer trivially copyable, I don't know the performance implications that such dispositions may have. I am open to discussion of what's the best way of doing this with Capy. It doesn't have to be dispositions. Another case for dispositions would be supporting custom error types, like boost::system::error_code. ATM this doesn't compile: capy::task<boost::system::error_code> my_task() { co_return {}; } capy::task<void> co_main() { co_await capy::timeout(my_task(), 1s); } I personally don't need this. Thanks, Ruben. [1] https://github.com/anarthal/nativepg/blob/master/include/nativepg/extended_e... [2] https://github.com/anarthal/nativepg/blob/master/include/nativepg/connection... [3] https://github.com/anarthal/nativepg/blob/master/include/nativepg/sqlstate.h...
śr., 24 cze 2026 o 11:50 Ruben Perez <rubenperez038@gmail.com> napisał(a):
3) Asio has the concept of error dispositions [6]. In a nutshell, these
are
a generalization of error codes, so custom error types can be used in error code-aware functionality, like when_any. This is useful in code using specialized types. Future candidate Boost.Http uses this in its router [8], and I intended to use it as a way to attach error messages in my database libraries. I discuss my use case in more detail in this issue [9]. Have you considered the feature?
[6]
https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/Disposit...
Would you be able to provide an example illustrating how you would use this?
My use case is including a diagnostic string returned by the server in the error type. For instance:
struct extended_error { std::error_code code; std::string diag; }; // complete type in my postgres library here [1]
Then you can define your Asio function like this:
class connection { public: template <asio::completion_token_for<void(extended_error)> CompletionToken> auto async_connect(const connect_params& params, CompletionToken&& token); }; // complete type here [2]
With Capy, I currently have:
class connection { public: // *diag will be populated in case of server error boost::capy::io_task<> connect(connect_params params, std::string* diag = nullptr); };
Before anyone asks, I can go down the path of "only network errors are failures" and write:
// Succeeds if the server rejects the connection - populates server_errors in this case boost::capy::io_task<> connect(connect_params params, extended_error& server_errors);
This is adequate for low-level functionality, but becomes hostile as you build higher level functionality.
For now, I've encoded the SQLSTATE returned by the server as a std::error_code, so the user gets _some_ diagnostics even if they didn't use the output parameter [3].
Isn't it like lower layers of the server deciding what the users will see in the API? Regards, &rzej;
Because extended_error is no longer trivially copyable, I don't know the performance implications that such dispositions may have. I am open to discussion of what's the best way of doing this with Capy. It doesn't have to be dispositions.
Another case for dispositions would be supporting custom error types, like boost::system::error_code. ATM this doesn't compile:
capy::task<boost::system::error_code> my_task() { co_return {}; } capy::task<void> co_main() { co_await capy::timeout(my_task(), 1s); }
I personally don't need this.
Thanks, Ruben.
[1] https://github.com/anarthal/nativepg/blob/master/include/nativepg/extended_e... [2] https://github.com/anarthal/nativepg/blob/master/include/nativepg/connection... [3] https://github.com/anarthal/nativepg/blob/master/include/nativepg/sqlstate.h...
Ruben Perez wrote:
6) Can I write types that satisfy ConstBufferSequence or DynamicBuffer without explicitly depending on Capy? This would be useful for low-level protocol libraries that want to take as few dependencies as possible. Setting the concrete const_buffer and mutable_buffer types apart, the usual way of representing "raw bytes" would be span<const unsigned char>/span<std::byte>. For instance, std::as_bytes [12] uses std::byte. If I got the reasoning behind ConstBufferSequence right [13], the purpose is to avoid allocations with vectored I/O. I'd say it's reasonable to allow std::array<std::span<const unsigned char>> to satisfy ConstBufferSequence.
Note that I have read The Span Reflex [14]. I agree with the need of ConstBufferSequence. While I personally don't agree with the need of the concrete const_buffer and mutable_buffer types, I understand the rationale of avoiding std::byte. I'm not challenging these types, only asking for better compatibility with std::span.
I've never been particularly fond of the Asio requirement that buffers need to be convertible specifically to the concrete types const_buffer and mutable_buffer. (Among other things, this requires physical coupling.) It being 2026, maybe we should explore the more principled concept MutableBuffer; concept ConstBuffer; concept MutableBufferSequence = MutableBuffer or (Range of MutableBuffer); concept ConstBufferSequence = ConstBuffer or (Range of ConstBuffer); That's what we already have, so it's not that big of a leap; the only difference is how the MutableBuffer and ConstBuffer concepts are specified (via the convertibility requirement.) If we formalize the above, we'd then be able to explore more relaxed ConstBuffer and MutableBuffer concepts (ones that structurally match span<unsigned char> and span<byte>, for example.)
On Wed, Jun 24, 2026 at 8:34 AM Peter Dimov via Boost <boost@lists.boost.org> wrote:
I've never been particularly fond of the Asio requirement that buffers need to be convertible specifically to the concrete types const_buffer and mutable_buffer.
I like it
(Among other things, this requires physical coupling.)
Not necessarily.
It being 2026, maybe we should explore the more principled
concept MutableBuffer; concept ConstBuffer;
No I don't like that at all. A sequence concept makes sense. A concept for a pointer and size pair seems overkill. Note, whatever definition you give the concept, we could simply add as an explicit constructor to const_buffer and mutable_buffer, and then the convertibility requirement would be satisfied. This way we don't need the full strength of an explicit concept, yet we can still get the benefits. This is the approach Asio uses (const_buffer and mutable_buffer are constructible from span). Thanks
On Wed, Jun 24, 2026 at 9:00 AM Vinnie Falco <vinnie.falco@gmail.com> wrote:
This is the approach Asio uses (const_buffer and mutable_buffer are constructible from span).
Here: https://github.com/boostorg/asio/blob/cba4d9791d77264814eccb53324d5d37669467... Thanks
Vinnie Falco wrote:
On Wed, Jun 24, 2026 at 8:34 AM Peter Dimov via Boost <boost@lists.boost.org <mailto:boost@lists.boost.org> > wrote:
I've never been particularly fond of the Asio requirement that buffers need to be convertible specifically to the concrete types const_buffer and mutable_buffer.
I like it
(Among other things, this requires physical coupling.)
Not necessarily.
Yes it does.
It being 2026, maybe we should explore the more principled
concept MutableBuffer; concept ConstBuffer;
No I don't like that at all. A sequence concept makes sense. A concept for a pointer and size pair seems overkill.
Note, whatever definition you give the concept, we could simply add as an explicit constructor to const_buffer and mutable_buffer, and then the convertibility requirement would be satisfied.
You need the definitions of const_buffer and mutable_buffer to be available in order to see whether they have constructors from this or that, which requires physical coupling.
This way we don't need the full strength of an explicit concept, yet we can still get the benefits.
I have no idea what "full strength" there is. A concept is a concept, it's not any more complicated or strong than a concrete type. You already have the concept, you just don't name it. "Convertible to const_buffer" is as much a concept as any other, and there's no "full" or "partial" strength involved anywhere. Sticking your head in the sand and pretending you don't have buffer concepts doesn't magically make them cease to exist.
On Wed, Jun 24, 2026 at 10:21 AM Peter Dimov <pdimov@gmail.com> wrote:
You need the definitions of const_buffer and mutable_buffer to be available in order to see whether they have constructors from this or that, which requires physical coupling.
If const_buffer was constructible from span<byte> then this is a perfectly valid buffer sequence which does not require physical coupling: array<const_buffer, 3>
This way we don't need the full strength of an explicit concept, yet we can still get the benefits.
I have no idea what "full strength" there is. A concept is a concept, it's not any more complicated or strong than a concrete type.
A concept is stronger than a concrete type because a concrete type comes in exactly one formulation while types which satisfy a concept comes in infinite formulations.
You already have the concept, you just don't name it. "Convertible to const_buffer" is as much a concept as any other, and there's no "full" or "partial" strength involved anywhere.
"Convertible to X" feels qualitatively different to me than a concept, as one is scoped and the other is unbounded. Yet I accept your framing, and "convertible to X" is what Asio does, and what we will do, so we are agreeing. Thanks
Vinnie Falco wrote:
On Wed, Jun 24, 2026 at 10:21 AM Peter Dimov <pdimov@gmail.com <mailto:pdimov@gmail.com> > wrote:
You need the definitions of const_buffer and mutable_buffer to be available in order to see whether they have constructors from this or that, which requires physical coupling.
If const_buffer was constructible from span<byte> then this is a perfectly valid buffer sequence which does not require physical coupling:
array<const_buffer, 3>
I assume you mean array<span<byte const>, 3> here.
This way we don't need the full strength of an explicit concept, yet we can still get the benefits.
I have no idea what "full strength" there is. A concept is a concept, it's not any more complicated or strong than a concrete type.
A concept is stronger than a concrete type because a concrete type comes in exactly one formulation while types which satisfy a concept comes in infinite formulations.
Right. So the set of types considered buffers is closed, because const_buffer only has a finite set of constructors, which is unlike a concept that can be satisfied by an unbounded set of user-defined types that do not require your explicit constructor-based blessing. And you consider this an argument in favor.
On Wed, Jun 24, 2026 at 10:34 AM Peter Dimov <pdimov@gmail.com> wrote:
I assume you mean array<span<byte const>, 3> here.
Yes
...the set of types considered buffers is closed, because const_buffer only has a finite set of constructors
Not if the constructor is a template. I realize that this is heading in the direction of being functionally equivalent to the concept yet I still prefer const_buffer and mutable_buffer as the delivery vehicle rather than the named concept. If for no other reason, that Kohlhoff did it that way and without knowing his rationale I lean towards doing the same. Thanks
Vinnie Falco wrote:
On Wed, Jun 24, 2026 at 10:34 AM Peter Dimov <pdimov@gmail.com <mailto:pdimov@gmail.com> > wrote:
I assume you mean array<span<byte const>, 3> here.
Yes
...the set of types considered buffers is closed, because const_buffer only has a finite set of constructors
Not if the constructor is a template. I realize that this is heading in the direction of being functionally equivalent to the concept
It is. Whether you define the concept indirectly by the overload set of the constructors, or directly, doesn't make much of a difference. The latter is just both more honest and better documented. Concepts exist even if you never have the keyword "concept" in your code or your documentation. They are just implicit.
On 24 Jun 2026 20:38, Vinnie Falco via Boost wrote:
On Wed, Jun 24, 2026 at 10:34 AM Peter Dimov <pdimov@gmail.com> wrote:
I assume you mean array<span<byte const>, 3> here.
Yes
...the set of types considered buffers is closed, because const_buffer only has a finite set of constructors
Not if the constructor is a template. I realize that this is heading in the direction of being functionally equivalent to the concept yet I still prefer const_buffer and mutable_buffer as the delivery vehicle rather than the named concept. If for no other reason, that Kohlhoff did it that way and without knowing his rationale I lean towards doing the same.
I suspect one reason is that concepts as a core language feature did not exist when ASIO was designed. Which is not a good reason for a newly designed library targeting modern C++.
On Wed, Jun 24, 2026 at 11:37 AM Andrey Semashev via Boost < boost@lists.boost.org> wrote:
I suspect one reason is that concepts as a core language feature did not exist when ASIO was designed. Which is not a good reason for a newly designed library targeting modern C++.
Let me dispel that misconception. Here is the ConstBufferSequence concept definition: https://www.boost.org/doc/libs/1_36_0/doc/html/boost_asio/reference/ConstBuf... This is the 2013 version of the library, seven years before the `concept` keyword was added to C++20. The "concept" for the value type of buffer sequences is what Peter said: ConvertibleToConstBuffer (also located on the page) Thanks
On 24 Jun 2026 22:21, Vinnie Falco wrote:
On Wed, Jun 24, 2026 at 11:37 AM Andrey Semashev via Boost <boost@lists.boost.org <mailto:boost@lists.boost.org>> wrote:
I suspect one reason is that concepts as a core language feature did not exist when ASIO was designed. Which is not a good reason for a newly designed library targeting modern C++.
Let me dispel that misconception.
Here is the ConstBufferSequence concept definition:
https://www.boost.org/doc/libs/1_36_0/doc/html/boost_asio/reference/ ConstBufferSequence.html <https://www.boost.org/doc/libs/1_36_0/doc/ html/boost_asio/reference/ConstBufferSequence.html>
This is the 2013 version of the library, seven years before the `concept` keyword was added to C++20.
The "concept" for the value type of buffer sequences is what Peter said: ConvertibleToConstBuffer (also located on the page)
As Peter said, concepts as a concept (sorry for the pun) existed long before they were added as a language feature. Though the documentation described the ConstBufferSequence concept, the implementation could not have used it. Even later, when concepts were added to C++, the implementation could not be changed easily because that could break existing users. This is not a consideration for a brand new library.
participants (7)
-
Andrey Semashev -
Andrzej Krzemienski -
Klemens Morgenstern -
Peter Dimov -
Ruben Perez -
Steve Gerbino -
Vinnie Falco