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* or char*: 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, and std::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:

  1. Some algorithms traverse buffers backwards

  2. The slice views from buffer_slice and consuming_buffers::data() must adjust the first and last buffers' bounds