Rationale
Why void* rather than std::byte?
Two concrete forces decide it:
-
The platform types already use it. The OS structures Capy maps onto are
void*orchar*:iovec’s `iov_base,WSABUF’s `buf. The pointer field never needs reinterpreting. -
Callers supply many element types. Data arrives as
char[],unsigned char[],std::byte[],std::string, and more. One neutral pointer erases all of them to a single representation.std::span<std::byte>would make every caller reinterpret first, andstd::span<void>is ill-formed.
This is an argument from layout and from caller convenience, not from semantics. "Raw memory" describes std::byte just as well as it describes void*.
Why concepts rather than one span type?
std::span<std::span<std::byte>> is the reflexive answer for multiple buffers, and it works. The problem is composition. Joining a two-buffer header to a three-buffer body means building a new five-element array, so every composition allocates:
HeaderBuffers headers{ /* ... */ };
BodyBuffers body{ /* ... */ };
// To combine, you MUST allocate a new array:
std::array<std::span<std::byte const>, 5> combined;
std::copy(headers.begin(), headers.end(), combined.begin());
std::copy(body.begin(), body.end(), combined.begin() + 2);
write_data(combined);
A concept accepts the composite directly, with no allocation and no overload per shape.
At type-erasure boundaries the tradeoff reverses. Virtual functions need concrete types, so Capy converts to concrete descriptors internally and keeps concepts at the user-facing edge.
Why Bidirectional?
The concepts require bidirectional ranges, not merely forward ranges, for two reasons:
-
Some algorithms traverse buffers backwards
-
The slice views from
buffer_sliceandconsuming_buffers::data()must adjust the first and last buffers' bounds