On Wed, Jun 24, 2026 at 7:08 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Here is my formal review of Capy.
Thank you for the detailed review, Rainer. I'll respond to each section. First, note that the submission is one logical library which is separated into two libraries physically. The purpose for the split was explained, and I'll repeat it here: *Why Capy Is Separate* https://develop.capy.cpp.al/capy/9.design/9b.Separation.html Corosio is intended to be the C++20 and later replacement for Boost.Asio, in that it offers a complete networking API. You did not review that part, a significant omission.
...I never even got Corosio to build...
What was the specific problem? <https://develop.capy.cpp.al/capy/4.coroutines/4d.io-awaitable.html>.
The only problem is that this example code does not work at all. It doesn't even compile. ... Based on the documentation, there is no way to get or create a working boost::capy::continuation, so there is no way to create a custom IoAwaitable.
The example on the *IoAwaitable* documentation page has a bug. It shows coroutine_handle<> where it should show continuation. That's our mistake and we apologize for the issue slipping through the cracks, and for the slow response on #296. However, the claim that "there is no way to get or create a working boost::capy::continuation" is incorrect. continuation is a plain data structure with a default constructor: struct continuation { std::coroutine_handle<> h; continuation* next = nullptr; }; Add a member to your awaitable of type continuation and set the coroutine handle. Then pass it to dispatch. The documentation is being corrected.
Capy coroutines cannot co_await awaitables from other coroutine libraries. Capy coroutines cannot co_await awaitables at all unless these awaitables also model the IoAwaitable concept. ...This is a literal truth and a deliberate design choice.
It is a deliberate design choice, and it is the library's strength. The compile error you get when co_awaiting a plain awaitable is type-level enforcement that the proper context - executor, stop token, frame allocator - will be available at the suspension point. This is explained in P4172 Section 5.1: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf
What this comes down to is that Capy wants to be the only coroutine library in your program.
It does not. Nothing prevents a task type's await_transform from accepting plain awaitables and wrapping them. The protocol does not forbid compatibility - it enforces that the transformation happens explicitly, through the promise type, where the environment can be properly threaded. A task author who wants to support foreign awaitables can do so by overriding transform_awaitable in their promise type (P4172 Appendix C). The choice is the task author's, not the protocol's. P4092R0 and P4093R0 demonstrate the boundary is crossable in both directions for std::execution senders. Capy provides a protocol. Protocols have boundaries. The boundary here is a compile-time check.
Running tasks requires a two-call syntax: run_async(pool.get_executor())(compute()); ..innocent-looking functions like the following do not work correctly:
void run_on_pool(auto task) { run_async(pool.get_executor())(task); }
The run_on_pool example is a lazy coroutine reference lifetime problem - universal to every lazy coroutine library. You acknowledge this yourself later in the review ("The same problem exists in TooManyCooks"). The task's reference parameters are already evaluated before run_async enters the picture. This is not a two-call syntax problem. The thread-local frame allocator coupling is a separate concern, and it is solved. P4172 Section 8.3 documents safe_resume: every executor event loop saves and restores the slot before and after resuming a coroutine handle. One pointer save/restore per .resume() call. Invisible to application developers. The two-phase call itself is a trade-off, not an accident. P4127R0 proves the design space is closed: operator new executes before the coroutine body, so the frame allocator must arrive either through the parameter list - allocator_arg_t at every call site, polluting every coroutine signature in the chain (P4172 Section 8.1) - or through out-of-band state. The two-phase syntax is the consequence of keeping application coroutine signatures clean. run_async and run are the launch functions we ship because we believe they are the most friendly. They are not exclusive. The library provides the *IoRunnable* concept (P4172 Section 6.1) precisely so users who want a different trade-off can write their own launch functions. Corosio's tcp_server is one example - a custom launch function built on *IoRunnable*. It is worth a reminder, that you did not use Corosio. Only Capy. So I guess the argument can be made that Capy is /technically/ complete and
doesn't need any more synchronization structures because it covers all five categories. But that's not an argument that I will be making. I want an asynchronous shared_mutex.
You acknowledge this is not a reject-level flaw, and I agree it is a reasonable feature request. Capy is the foundation for Corosio. We have intentionally not tried to evolve it beyond the core. Our approach: scope the claim, ship the core. After it is in Boost and we get real user feedback, we explore what to add. Too many libraries provide the kitchen sink. We are first building the kitchen. Then we get the sink. Then we add the cooks. But not too many. TooManyCooks also lacks the async_shared_mutex you wanted. These are reasonable requests for post-acceptance work.
In fact, Capy doesn't seem to provide /any/ executors beyond thread_pool. TooManyCooks provides four of them
You chose to evaluate only Capy and explicitly declined to consider Corosio ("I won't, because I haven't examined Corosio in detail yet"). But Capy and Corosio are one comprehensive work physically split into two libraries. Capy provides the protocol and foundation; Corosio provides the I/O layer including concrete executors, reactors, and platform integration. Judging executor variety by looking only at Capy is like judging a networking stack by looking only at the domain resolution function. The *Executor* concept and execution_context base class (P4172 Section 5.2, 5.5) are the extension points. Custom executors are a first-class use case, not an afterthought. The comparison to TooManyCooks' resume_on is also apples-to-oranges. co_await run(ex)(task()) creates a new io_env for the subtask - new executor, inherited stop token and allocator. TMC's resume_on switches the current coroutine's executor without creating a new environment. Different trade-offs. The Capy equivalent to TMC's function is instead capy::run if I understand TMC correctly. I think that Capy in its current state is a trap. It cuts you off from the
wider world of C++ coroutine libraries
The protocol boundary is a compile-time check that ensures correct environment propagation. It can be opened through transform_awaitable (P4172 Appendix C) and bridged through await_sender/as_sender (P4092R0, P4093R0). It is not a trap. It is a type-level guarantee. The documentation bug on the *IoAwaitable* page is being fixed. The design choices - the two-argument await_suspend, the two-phase launch syntax, the scoped feature set - have published rationale in P4172. Reviewers interested in the full trade-off analysis will find it there. Thanks