wt., 7 lip 2026 o 23:38 Alan de Freitas via Boost <boost@lists.boost.org> napisał(a):
Boost.Capy Review
I want to thank the authors for their work.
This is a review of Boost.Capy only. It wasn’t an explicit decision to review them separately. It’s just how much I managed to review properly. I’ll start reviewing Corosio and send a separate review if there’s time, or independent feedback if I’m too late.
I haven’t been able to follow all the discussions in other reviews, so I imagine many of the points I’ll raise here may already have been addressed. I kindly ask for the review manager to account for that accordingly.
There’s a small appendix at the end of this review with a more detailed list of minor issues to make the main points easier to follow.
Does this library bring real benefit to C++ developers for real world use-case?
Yes. I’m excited about the possibility of exploring these coroutine implementation details not only to achieve better syntax but also to improve performance.
Do you have an application for this library?
Not right now, but I intend to use it once it’s accepted.
Does the API match with current best practices?
Yes. To a certain extent, the library also proposes its own best practices. So, for a subset of those practices, only the future will tell. As others have noted, the “Two-Call Syntax” is a bit controversial, and I hope there’s a better solution. At the very least, the warning admonition needs to list more dangerous cases.
I think the split into two libraries here is also a little controversial and not well articulated (I haven’t followed all previous discussions you guys had about that). My impression is that the decision was based on conditions that no longer apply (probably something about dependencies), and that some of the justification now is being formulated in retrospect, which seems weird to me. I suspect the authors originally expected different dependencies for each, but that is no longer a problem now that they’re both standalone libraries. Splitting functionality also doesn’t make much sense here in my opinion, because (i) Capy is intended to be used only with Corosio (which was a point against splitting it), and (ii) there’s so much functionality in these libraries that any particular split into only two libraries would be somewhat arbitrary. If it can be used without Corosio, then Corosio shouldn’t be used to justify missing features to people who request them, and the documentation should explain the explicit decision from the very beginning. Otherwise, it seems like the argument for the split keeps moving the goalposts (things are missing, and it’s not independent, but that’s OK because it’s intended for Corosio / it deserves to be a separate library because it’s independent from Corosio).
Is the documentation helpful and clear?
The documentation is incomplete (in a sense, too “complete,” because much of the work needed is removing AI fluff). Examples that don’t compile, etc. The pages intended as exposition often replicate a partial, exhaustive reference instead. If I understand correctly, the authors were working on it and had to freeze it for the review, but I’m sure they will fix this.
On the use of AI:
The way AI was used here makes the documentation very hard to read. I left a more detailed list of issues at the end of the review but, to be clear, I have nothing against AI. I just think to some extent it wasn’t used properly here. We’re going to disagree on the specific list but in general:
- AI is good at summarizing content, bootstrapping projects, performing large mechanical refactorings, and fixing bugs that are easy to fix but difficult to reproduce. - AI is bad at expanding content with actual information (it begins to generate fluff between statements and breaks hierarchical consistency), understanding important contextual subtleties (in particular, personal, unspoken, and often emotional), producing good technical writing in general, doing strategic work, maintaining hierarchical consistency in a project, ensuring solutions are maintainable, and keeping them that way.
The last point is particularly important because, for large projects, the cost of maintaining it is orders of magnitude greater than the cost of bootstrapping it. If we throw AI at that, too, the code and documentation will keep drifting, finer details will never get fixed, and we’ll keep seeing regressions. By “throw AI at that,” I mean letting it do most of the work on issues or relying on it to maintain consistency. Accepting one of the suggestions it recommended for a small problem after properly reviewing them line by line seems fine for many local issues.
In other words, it’s important to notice that AI might be helpful when bootstrapping code or documentation but not for maintaining code that doesn’t drift. The use of AI here so far seems to have been reasonably good for the code (the code is also more verifiable than writing, and people seem reasonably happy with it), but not so good for the documentation, though that can be fixed. But if AI is used again to fix the issues that come up during this review and after that there’s no lock into some kind of stable version that will be maintained with a more active human in the loop, any review will be pointless because more and more AI will just drift to another version of the documentation and code with the same problems. In the case of documentation, it’s not only about drifting because I think AI, or at least the current models we have, is simply incapable of fixing it by itself.
What’s interesting is that I often wouldn’t understand something in the documentation, so I would get help from an AI agent. Even though both explanations are from AI, I think the other agent gave me a better explanation because they were forced to provide it without the session context being polluted by other assumptions. This might be a hint into how to improve things a little if we purposefully use agents without context, making them more representative of how the reader arrives at understanding.
On the format:
I also think the documentation is orders of magnitude longer than it needs to be, given the repetition and concepts from the reference that are exhaustively replicated in the exposition simply because AI can do it cheaply (many already drifting from the reference). For instance, I could easily see how the few concepts of actual information across all 7 pages under “Buffer Sequences” (Buffer, Buffer sequences, etc.) could be presented on a single page, with one section for each, along with brief admonitions for the design rationale.
A side note about the process:
My final evaluation is that the documentation is very hard to read due to AI fluff, and that carefully reading the documentation and the code is impossible within the 10-day review period. I mean “impossible” literally, as any reasonable words-per-minute or LOC-per-minute calculation. But in the general case, this has little to do with the submission, or even AI. And the authors are not to blame for that but should be part of an independent discussion I’d like to propose.
I believe this is the longest boost submission ever under review, and it’s many times larger than Asio and Beast were when they were accepted (I checked). We have had review extensions for much smaller libraries. Ideally, instead of asking for extensions, we could have a process that already accommodates larger libraries based on their size, though I have no idea what that would look like. Otherwise, we’re either going to have (i) only reviews by people who were already casually using the library, or we’re going to have (ii) a battle of people using AI agents to pretend they’re reviewing code written by other AI agents.
It would also be nice to confirm with the authors (maybe this has already happened) that the documentation is in a state they find acceptable for review before starting the review, so they don’t freeze the documentation in an intermediate state from another milestone in the project.
Did you try to use it? What problems or surprises did you encounter?
Yes. I compiled the code and ran the examples.
What is your evaluation of the implementation?
I didn’t have time to properly evaluate the implementation line by line. I have a list of notes I left at the end. I mostly assumed the API as presented in the documentation to write that.
Please explicitly state that you either *accept* or *reject* the inclusion of this library into boost.
I recommend accepting the library. I don’t want to make it conditional, since I know the authors will take all comments into account, which makes it redundant.
Also, please indicate the time & effort spent on the evaluation and give the reasons for your decision.
I read the documentation but not the whole reference. I skimmed through the source code. I compiled the library and ran the examples. I spent most of the last three weeks reviewing the library.
Disclaimer
I'm affiliated with the C++ Alliance.
My complete list of minor issues, notes, and suggestions so it doesn’t pollute the rest of the review
As I mentioned, “I haven’t followed all the discussions in other reviews, so I imagine many of the points I’ll bring up here have already been discussed.”
- The documentation is clearly incomplete. Examples that don’t compile, etc. If I understand correctly, they were working on it and had to freeze it for the review, but I’m sure they will fix this. - The way AI was used here makes the documentation very hard to read. (i) The same concept or part of it is often phrased differently over and over without adding any new information. Very often, this is done via unnecessary negatives (“This is X. Not Y. Not Z”, instead of “This is X”). (ii) There are too many clichés and metaphors that constitute bad technical writing because they force the reader to keep reverse-engineering them (although one per page could be useful to make something more memorable). (iii) Expressions are used in the text without a proper definition, probably because they came from the conversation context with the agent. (iv) The hierarchy of the pages and sections is often incoherent. This is not a problem with AI per se. The problem is bad writing style. We just happen to know AI is the cause. - I don’t like the introduction to coroutines because it’s syntax-based (listing what each command does before presenting the motivation) rather than goal-oriented (listing typical use cases regardless of coroutines and explaining how coroutines help with each one until all the syntax is covered). - The examples could include something slightly practical rather than a coroutine that literally does the thing it’s explaining. For instance, the `final_suspend` could be an example that shows the reader something they can relate to as achieving a goal, no matter how simple it is. - The examples that used pseudocode with the same keywords as the real syntax, mixed with the real syntax, were confusing. It’s easy to lose track of what a keyword means in a specific example. - The hierarchy of the introduction to coroutines is incoherent. “Advanced Topics” (which only discusses Symmetric Transfer) is presented before await_suspend is properly explained in the previous pages. Even within the same page, the “The Solution: Return the Handle” is presented before the section “Return Types for await_suspend” that presents which return types are valid and what they do. - In the HALO section, the sentence "The coroutine type is marked with (Clang extension)" renders with a blank where the attribute name should be. The HTML contains - The 3 topics in Advanced Topics could have their own page, and so they can be explained properly. The distinction between Advanced Topics and other topics could be removed. - My impression is that the writing styles of the introductions to coroutines and to concurrency are too different (and the introduction to concurrency is better). The introduction to concurrency explains very basic concepts very well. Concepts most C++ developers are likely already familiar with. On the other hand, the introduction to coroutines uses very big words without bothering to explain them. For instance, there’s a whole page about std::threads, everything related to it, and working examples on how to use it in different use cases. Meanwhile, Symmetric Transfer, a concept that’s way more complex and much more consequential to the library and much harder to visualize, is explained with two very short paragraphs. - The section titles in the Concurrency introduction are not really appropriate, though. They could just reflect what they describe: I: Threads, II: Mutexes, etc. Otherwise, it seems to promise much more than it actually offers. - The paragraph on spurious wakeups could briefly explain why they exist. - The documentation hierarchy is inconsistent in how it presents the design rationale. Some pages are dedicated to it; there’s also a set of pages at the end dedicated to it; and sometimes it’s presented interleaved with the text. The documentation should choose one of them. My personal preference is for admonitions interleaved with the text, because the user has the necessary context and can choose to ignore them. The other options are easily ignored and often annoying because the text doesn’t get to the content the user is looking for. - The documentation exposition should use the cpp: macro so that we get links to the reference. This is very useful so I can see what a class looks like when reading about it for the first time. - The documentation is also not goal-oriented when it’s explaining concepts about Capy. For instance, the page “The task Type” has coroutines that return capy::task but no example where the task is executed. A user following the documentation and compiling small examples as they learn has nothing they can run. The examples could just contain whatever the simplest way to run these tasks is (maybe just running them inline). - The reference of capy::run_async, if it remains like this after review, is incorrect. It says “Asynchronously launch a lazy task on the given executor”. But it doesn’t do that because it doesn’t receive any task. What it does is create the intermediary object the library requires for that. - The “Two-Call Syntax” section doesn’t explain the design rationale well here. The necessity here is just assumed, but the user doesn’t know the implementation details that justify it or the tradeoffs of the alternatives. The logic might be sound, but the documentation doesn’t explain it well. In any case, the syntax looks quite ugly and dangerous. I hope you guys find a better solution to this. There could also be a reference from this page to the “Frame Allocators” page, where there’s a little more detail on why things have to be this way. - “When no result handler is provided, the result is discarded” → Couldn’t it just return the result when no handler is provided (and throw the exceptions)? This would make coroutine code look a little more like regular code. I know some examples show the value being handled in the executor, but for many use cases we might not really need that. - I find the name “run” a little weird for this function because what’s special here is the context transfer more than the fact that something is being run, which could be done most of the time by just calling the function. - The reference sections in the exposition pages don’t look very good. We already have a real reference. And if the information about the headers is important (they are), they should be explained right at the beginning. As we’re incentivizing the user to include what they use and this information is important, the examples should also not include boost/capy.hpp directly. - I understand what the code is doing, but I find it a bit weird that we have the same name for different kinds of symbols execution_context and ExecutionContext where the distinction is only the casing. This seems like a smell to me. - The executor concept doesn’t enforce that ce.context() returns a real context. - The exposition attempts to do the role of the reference and replicate the full concepts but it’s drifting and out of date. - There are simpler ways to express these requires clauses of the Executor concepts because they’re just a conjunction. - “Executors and Execution Contexts” needs much better examples for everything. - I find the concept of an executor_ref a little weird, because I thought executors were already almost sort of reference types within the executor context. Then I know this function is more about type-erasure than about creating reference types, but that’s another problem because we have already been using \*\_ref for reference types in boost libraries, such as in boost::urls::segments_ref. I think the typical name would be any_executor or something like that but I’m sure they considered that and discarded the idea for some reason. - The section “execution_context: Base Class” is a little out of place. The documentation is listing executor contexts the library provides, and this section is interleaved there. But this section is about writing your own contexts. This should come before or after the list of contexts that come with the library. - Some pages are quite long, and I miss the ToC on the right-hand side on the Antora template. Even if it were optional. - I found the page on “The IoAwaitable Protocol” a little confusing because the properties described here don’t seem specific to I/O. The confusion seems to stem from this ambiguous and partly retroactively justified relationship between Capy and Corosio. But the fact is that after reading about 35% of the documentation, the reader doesn’t once see any mention of I/O. And most of the content in the documentation after this page also doesn’t mention I/O at all until we get to the next parent section. So, to the reader, it seems like the library doesn’t have any special entanglement with I/O, although it might support it, and then the reader has to completely change what they are supposed to expect after having been reading the documentation for hours or days, depending on how deeply they’re reading it. What’s even more confusing is that this page doesn’t mention I/O much at all. The only mention of I/O on this page is in a context that reexplains the concepts supported by the run_async function. So, if this page content is complete, it seems like the concept could simply not be attached to I/O because we also need executors, stop tokens, and allocators in other applications. “But real I/O code needs more:” could just say “But real applications needs more:” and the concept would still be correct with no mention to I/O. - The section “The IoAwaitable Protocol” could just have examples of the environment being propagated (in other words, it could just explain how to use it) before describing how it works and how trying to duplicate the reference. In fact, these duplicates of the reference in the exposition are not only unhelpful but also prone to drifting very easily. AI gives the impression that it’s now cheap to write, but this doesn’t include the cost of maintaining it. - “A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept simply does not test for them”. Why not just test for them so the name matches the check? Or rename it so it also matches? - This whole page on The IoAwaitable Protocol is very tiring. It’s full of implementation details, then a custom class, but not a single example of a task running and benefiting from any of this. Without an example, it’s hard to explain in what context each of these functions would be called. I think this criticism applies to almost everything in the documentation, so I’ll try to stop repeating myself. - The documentation has good example snippets that explain how to use well-known functionalities of the STL (like std::stop_token), but it doesn’t have examples of the same level of quality for the library’s own utilities. My intuition is that this is because AI is already trained on existing things, so it could create good content for existing concepts and didn’t generate content as nice for the new concepts. For instance, the examples for std::stop_token exemplify std::stop_token without coroutines until much later. Much of this “Part” of the page could be in the initial tutorials. - “Stop Tokens and Cancellation” contains a large heading called “Part 4” and it’s a single paragraph. This doesn’t make much sense. If it’s just a small detail, there’s no reason to call it a “Part” and make a big deal about it. To be honest, I don’t see what’s special about this page to make it deserve explicit “Part”s instead of regular headings. - For when_all and when_any, it’s not clear to me from the examples how to communicate non-exceptional success/failure and what the relationship with stop tokens is (if any) for the assumptions about success and failure so that other tasks can be canceled accordingly. - For when_all and when_any, I thought the typical type to represent a regular void would be std::monostate rather than tuple<>. - The section explaining when_any casually mentions io_task without any previous explanation and I suspect the sentence is not even correct that this is required about the children. - As usual, the Frame Allocators page could start with the “Using Custom Examples” and use that as the starting point for later discussing the internal details. - In the example for “Scope Variables to Reduce Frame Size”, I couldn’t find where reply comes from. This seems important to make the code representative of something that needs the buffer. - “the compiler can reuse the same frame memory for both — even on Clang”: what does “even on Clang” mean here? - I find the discussion about variable scopes a little out of place here. It’s a didactic curiosity and it’s interesting. But it’s not about what the library provides and is not necessary to any library concept about to be explained after it. The statements also seem too circumstantial. - Information such as “GCC vs Clang Frame Sizes” is interesting but could be in admonitions. It would give the text better visual structure, which is missing, and orient the reader a little better. - The explanation in “Lambda Coroutine Captures” → “Why It Fails” has lots of steps unrelated to the problem. It seems like step 6 is related to the two-step pattern I hope there’s a fix for and at least an alternative solution that blocks things as expected even if the desired optimizations have to be disabled. The explanation also begs the question because it doesn’t really explain why the destruction of the outer moved-from implies the invalidation of the inner moved-to task. I think another problem here is the section “The Problem” is also using an example that’s IIFE, but because this concept is only presented later, the call is only `()`, and the task variable uses `auto`, it’s very easy to miss and at first glance think that `task` represents the lambda rather than the result of the invocation. - I thought capy::test::run_blocking would be available from capy:: even if with another name. It seems like something people do often. - The “Quick Reference” for "Lambda Coroutine Captures” should have the safe patterns first and the unsafe ones later. - The section "Scatter/Gather I/O” says the only alternative is to create copies, but the most obvious alternative to me is to call the function twice. It’s still not great, but it’s not the same. - I don’t know how other people feel about this, but “The Span Reflex for Multiple Buffers” didn’t occur to me at all. - The section “STL Parallel” seems to be claiming too much. Especially when the algorithm expects a range rather than iterators. - The section “Why Not std::byte?” is eliminating a possibility that should be eliminated right at the beginning. And the page is lacking examples, especially for things like “Capy provides `const_buffer` and `mutable_buffer` as semantically neutral buffer types. They have known layout compatible with OS structures (`iovec`, `WSABUF`) without imposing `std::byte` semantics”. There are no examples using these types. In any case, this is another interleaved design rationale page whose content could be interleaved with library content as admonitions. - The explanation in “Why Not std::byte?” is contradictory. The claim is that std::byte says "this is raw bytes”. But the description for void\* that “The OS doesn’t care if the bytes represent text, integers, or compressed data” using the word bytes again is, in practice, claiming void\* also says "this is raw bytes”. The word “bytes” is literally that, and the fact that they are opaque representations for other things is literally the definition of “raw” in this context. It’s basically saying the same thing about std::byte and void\*, and using that to justify void\*. I’m not saying std::byte is the right choice. Only that this justification is not the best one. Also, section System I/O Integration describes that CHAR is used internally on windows. - The “Buffer Sequences” page and its contents are arriving a little too late. The previous pages all discuss buffer sentences even more that buffer types. - The example for “buffer_slice” doesn’t seem great because I have the impression you could probably do the same thing without the buffer_slice. The example uses the first parameter, but the explanation uses 3 parameters. - I find the name of the buffer_length function a little confusing. The input is a sequence rather than a buffer (buffer is a case of buffer sequence by the other concept but this is the inverse relationship) and the name plus its very existance makes it seem like it does something more special (the bytes) than just returning something like the “std::ranges::size”. “buffer_size” measuring bytes makes all of that 10x more confusing. Unless you’re using the library (or the one this pattern came from) too often, everyone will be looking at the documentation every time they need this operation. - “Capy’s Corosio library exposes these optimizations where available”: how? - “Dynamic Buffers” is once again replicating the API exaustively instead of providing examples and an explanation.
I wanted to share that I agree with a number of Alan's observations and he expressed it better than I ever could. 1. The submission is too big to be properly reviewed in three weeks by people having their daily jobs and private life. 2. The documentation of Capy (didn't even have time to start Corosio) gives an appearance of being complete and broad, but after reading it all I do not have an impression that I actually learned it. 3. A lot of duplication of material between the prose section and the reference section, and they diverge. For instance the concept definitions. I also wanted to communicate that I have recently started the work on improving the documentation for Capy, mostly the Reference section but it will naturally imply changing also the prose part. This is my exclusive assignment, so there is no risk that working on implementation details can starve the work on documentation. I usually tend to recommend the rejection of libraries in Boost Reviews, following the reasoning "first polish the library, then we will decide", or "I want to assess the final product rather than a (sound) promise". But I am always outvoted. Boost Reviews gravitate towards just accepting anything that "looks promising". In that vein, I would be more comfortable not giving an "accept" verdict just yet. Instead, give encouragement and support, and reconvene when the reported concerns (some resulting from bad communication (also in the documentation)) have been addressed. The only thing that makes me hesitate is that we didn't do that for other libraries in recent history. I think we have recently lowered the bar for Boost submissions acceptance. Regards, &rzej;