Re: Capy Review

Vinnie Falco via Boost <[email protected]> Wed, 24 Jun 2026 08:14:52 -0700
Newsgroups gmane.comp.lib.boost.devel
Message-ID <CA+EzHGfdxRE3aPSgptHqbhrpDq9FqAUgztXE5Ma4p0QqM1YY=Q@mail.gmail.com>
On Wed, Jun 24, 2026 at 7:08 AM Rainer Deyke via Boost <
[email protected]> 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
_______________________________________________
Boost mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://lists.boost.org/mailman3/lists/boost.lists.boost.org/
Archived at: https://lists.boost.org/archives/list/[email protected]/message/RA6YFQM7KZMAGHM325CSVW6L6ZGO4TDN/