Replies: 4 comments
|
Did you do a survey of open source projects which use awaitables to find out how many use arity of 3 or higher? |
The Tuple Stops at TwoEvidence from sixty codebases for splitting the byte-transfer result contract from the error-only contract Report type: Analytical / recommendation. Executive summarySplit the contracts. A five-way survey of open source C++ coroutine code, covering roughly sixty repositories across every major ecosystem, finds that byte-transfer operations ( The recommendation has three parts. First, specify the byte-transfer contract as its own named shape, Contents
1. The recommendation comes firstReserve the compound result for byte transfer. Give everything else an error-only result. The current design routes every asynchronous operation through one variadic product type, The evidence says the single shape conflates two contracts that never meet in practice. One contract is two-fold: a byte transfer always produces both a status and a count, and the count is meaningful even on failure, because a partial transfer is real data the caller owns. The other contract is singular: an operation either succeeds or fails, and on failure there is no payload to report. Operations in the second group that today return 2. The question is whether one product type can serve two contractsThe Capy family defines A secondary question concerns scope. 3. Five parallel surveys covered every major coroutine ecosystemFive research agents surveyed disjoint slices of open source C++ on 2026-09-19, each looking for the same pattern: a The method combined shallow clones of roughly forty repositories, grepped in full with multiline regular expressions, with GitHub code search, Sourcegraph, and grep.app queries for the binding pattern. Every reported hit was verified by reading the actual source file, and each hit records the operation, its declared result type, and the semantics of each element, with specific attention to whether trailing elements are meaningful when the leading status reports an error. Two limitations deserve disclosure. Sourcegraph and grep.app were partially blocked by bot walls during some sweeps, so long-tail coverage of personal forks is incomplete. And absence claims are bounded by the search pattern: an operation returning a named struct rather than a tuple-like would not match, which is why the surveys also recorded what each ecosystem's accept, resolve, read, and write operations actually return. 4. Findings: four shapes, and only one of them is byte transferThe surveys found that three-or-more-element Table 1. The four shapes of multi-element
4.1 Byte-transfer results never grow past two elementsThe strongest finding is a negative one. Across Asio, Beast, Boost.Redis, Boost.MySQL, yalantinglibs, seastar, async_simple, libcoro, and every application project examined, no byte-transfer operation returns more than (error, count). Beast's entire surface tops out at The two-fold shape is also semantically stable: the count is meaningful on every path. A read that transfers five hundred bytes and then hits 4.2 Combinators produce the widest tuples, and every element is meaningfulThe largest destructurings found are combinator results. ScyllaDB destructures seven elements from Two properties set this shape apart. The arity equals the number of children, so the tuple size is a function of the call site, not the operation. And the elements are per-child payloads, not one operation's compound result. Capy's 4.3 Envelope operations carry error slots, and the trailing elements are dummiesThe third shape is the one the design question predicted. libcoro's UDP Every ecosystem that ships this shape invented its own answer to the same problem: how to report an error from an operation that also has payload slots. The answers disagree (leading status, trailing optional, disengaged optional, 4.4 One genuine counterexample family exists: protocol operationsThe surveys found one family that breaks the dichotomy honestly. async-mqtt5's operations carry multi-value completion signatures: 5. Discussion: the dichotomy is transfer versus non-transfer5.1 The tuple is honest exactly where the operation is two-foldThe survey evidence reframes the original claim slightly. The natural split is not "streams versus everything else" but "byte transfer versus everything else." Stream reads and writes, positional file reads and writes, and datagram sends and receives all share the two-fold property: every completion, success or failure, produces both a status and a count, and both are meaningful. This resolves the secondary question from section 2. Outside the transfer family, no operation in sixty repositories produces a compound result where the payload is meaningful on failure. The operations that carry extra tuple elements anyway - libcoro's UDP peer, the 5.2 Dummy values are a confirmed design smell, not a hypothetical oneThree independent codebases ship the dummy-on-error pattern in production or near-production code, and each signals it differently. libcoro documents it in the doc comment ("if ok then also the peer who sent the data and the data"). ScyllaDB gates the payload behind an explicit 5.3
|
| Survey slice | Repos examined | Verified hits | Dominant shape |
|---|---|---|---|
| Asio / Beast / Capy / Corosio | 11 cloned + cross-checks | 41 | parallel_group, MQTT envelopes, capy::when_all |
| Folly / libunifex / stdexec / beman | 11 cloned | 12 | collectAll family (test code) |
| async_simple / cppcoro / libcoro / yalantinglibs | 10 cloned | 20 (12 vendored asio) | when_all, libcoro UDP envelope |
| Cobalt / QCoro / seastar / drogon | 5 cloned + 3 searched | 15 | coroutine::all, RPC envelopes |
| Broad sweep | 16 cloned + per-repo searches | ~55 repos with hits | when_all family, domain tuples |
Method limitations, restated from section 3: Sourcegraph and grep.app were partially bot-blocked; named-struct results would not match the binding pattern; fork coverage is incomplete. None of these limitations cuts against the findings' direction, because each limitation hides potential hits rather than manufacturing them, and the hits that exist already sort into the four shapes.
2026-09-19 17:15 - kimi-k3
|
Looks like you have already done the analysis you requested. Looks like a lot of libraries has the same problem and use the wrong tool for the job. |
|
A peculiar constraint we are under is that we cannot use other Boost libraries in the implementation. This disqualifies Boost.System, which exists to handle exactly this very problem. Options I can see right now: Option 1. Leave Option 2. Reimplement Option 3. Wrap in a dedicated class template the current error sum type cases. That would be a very minimalist implementation of |
Uh oh!
There was an error while loading. Please reload this page.
While the naming is revised separately, I would like to rehash the idea of returning a product type of
error_codewith other things.It looks to me like the idea of returning
tuple<error_code, size_t>only makes sense for stream reads and writes. By streams, I meanReadStream,WriteStreamand their derivatives:Stream,ReadSource,WriteSinkandread_some_atinrandom_access_file(which I am surprised not to find part of the stream concept system). Any other function in the Capy-family useserror_codeto represent either error or data.While more functions return
tuple<error_code, something>, thesomethinguses dummy, meaningless values upon non-zeroerror_code. This is looks like a desperate attempt to fit into thetuplethat is inadequate in these contexts.In all occurrences of
io_result<>its number of arguments is either zero or one. In the cases where it is one:size_t, orerror_codeis non-zero, in order to try to usetupleas a sum type.So, this use of a tuple is an overreach. It blurs the difference between funcitons signalling an error, and streams reads/writes returning a two-fold information. This is also why
when_allandwhen_anycannot be designed satisfactorily.I would recommend separating the stream read/write contract from other functions' contract.
All reactions