On 6/24/26 17:14, Vinnie Falco via Boost wrote:
On Wed, Jun 24, 2026 at 7:08 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
...I never even got Corosio to build...
What was the specific problem?
https://github.com/cppalliance/corosio/issues/260 (apparently fixed now).
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.
So I can just leave the 'next' member as nullptr, and everything works? That's not clear from the documentation at all. That would have been helpful to know three weeks ago.
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.
I know it's deliberate. That doesn't make it any less restrictive.
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.
So your answer is to write a custom promise type, and therefore a custom task type (because the promise type is a part of the task type). My answer is to write a wrapper function that can be called at the point where the IoAwaitable is needed: boost::capy::task<> f() { co_await to_io_awaitable(awaitable_from_other_library()); } I like my approach better, although I don't know how viable the to_io_awaitable function is. Or why Capy doesn't provide it, if it is. Awaiting an awaitable is an implementation detail of the function 'f' here. It should have no effect on the function signature. 'f' can even be an override of a virtual function, in which case the function signature can't be changed anyway.
P4092R0 and P4093R0 demonstrate the boundary is crossable in both directions for std::execution senders.
Yes, I believe I mentioned a similar way of crossing the boundary in very review:
It is also possible to post non-Capy tasks to a non-Capy executor from a Capy coroutine, and conversely, to post Capy tasks to a Capy executor from non-Capy coroutines (but see Synchronization below).
What I am concerned is the ergonomics, and the performance, of crossing this boundary. Because this looks a lot like the chain of callbacks that coroutines were invented to avoid.
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.
There are no (visible) reference parameters in my example. boost::capy::task<void> f() { } void run_on_pool(auto task) { boost::capy::run_async(pool.get_executor())(task); } run_on_pool(f());
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.
Yes, different tradeoffs. Nothing *wrong*, per se, with Capy's approach. But I am allowed to prefer TooManyCooks' approach, and I am allowed to say so in my review. Coroutines are essentially syntax sugar for untangling a chain of callbacks into a linear function. TooManyCooks' resume_on delivers on that promise. Capy's run does not. I would go so far as to say that Capy's solution doesn't provide any compelling reason for staying in a coroutine context at all. Instead of this: int result = co_await boost::capy::run(main_thread_executor) ( [=]() -> boost::capy::task<> { // Code to run in main thread here } ); I could write this instead: functions_to_run_in_main_thread.push_back( [=]() { // Code to run in main thread here } ); I lose the ability to get a result back to the coroutine, but I was never going to be able to do anything useful with that result anyway because boost::capy::run_async swallows return values. If I want to use the result locally instead of returning it, I can do so within the callback.
The Capy equivalent to TMC's function is instead capy::run if I understand TMC correctly.
I'm don't know what distinction you are making here. I *am* using capy::run. Do you mean some other overload of capy::run? -- Rainer Deyke - rainerd@eldwood.com