Why Concepts, Not Spans
This section explains why Capy uses concept-driven buffer sequences instead of std::span, and why this design enables composition without allocation.
The I/O Use Case
Buffers exist to interface with operating system I/O. When you read from a socket, write to a file, or transfer data through any I/O channel, you work with contiguous memory regions�addresses and byte counts.
The fundamental unit is a (pointer, size) pair. The OS reads bytes from or writes bytes to linear addresses.
The Reflexive Answer: span
The instinctive C++ answer to "how should I represent a buffer?" is std::span<std::byte>:
void write_data(std::span<std::byte const> data);
void read_data(std::span<std::byte> buffer);
This works for single contiguous buffers. But I/O often involves multiple buffers�a technique called scatter/gather I/O.
Scatter/Gather I/O
Consider assembling an HTTP message. The headers are in one buffer; the body is in another. With single-buffer APIs, you must:
-
Allocate a new buffer large enough for both
-
Copy headers into the new buffer
-
Copy body after headers
-
Send the combined buffer
This is wasteful. The data already exists�why copy it?
Scatter/gather I/O solves this. Operating systems provide vectored I/O calls (writev on POSIX, scatter/gather with IOCP on Windows) that accept multiple buffers and transfer them as a single logical operation.
The Span Reflex for Multiple Buffers
Extending the span reflex: std::span<std::span<std::byte>>:
void write_data(std::span<std::span<std::byte const> const> buffers);
This works, but introduces a composition problem.
The Composition Problem
Suppose you have:
using HeaderBuffers = std::array<std::span<std::byte const>, 2>; // 2 buffers
using BodyBuffers = std::array<std::span<std::byte const>, 3>; // 3 buffers
To send headers followed by body, you need 5 buffers total. With span<span<byte>>:
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);
Every composition allocates. This leads to:
-
Overload proliferation�separate functions for single buffer, multiple buffers, common cases
-
Performance overhead�allocation on every composition
-
Boilerplate�manual copying everywhere
The Concept-Driven Alternative
Instead of concrete types, use concepts. Define ConstBufferSequence as "any type that can produce a sequence of buffers":
template<ConstBufferSequence Buffers>
void write_data(Buffers const& buffers);
This single signature accepts:
-
A single
const_buffer -
A
span<const_buffer> -
A
vector<const_buffer> -
A
string_view(converts to single buffer) -
A custom composite type
-
Any composition of the above�without allocation
Zero-Allocation Composition
With concepts, composition creates views, not copies:
HeaderBuffers headers = /* ... */;
BodyBuffers body = /* ... */;
// cat() creates a view that iterates both sequences
auto combined = cat(headers, body); // No allocation!
write_data(combined); // Works because combined satisfies ConstBufferSequence
The cat function returns a lightweight object that, when iterated, first yields buffers from headers, then from body. The buffers themselves are not copied�only iterators are composed.
STL Parallel
This design follows Stepanov’s insight from the STL: algorithms parameterized on concepts (iterators), not concrete types (containers), enable composition that concrete types forbid.
The span reflex is a regression from thirty years of generic programming. Concepts restore the compositional power that concrete types lack.
The Middle Ground
Concepts provide flexibility at user-facing APIs. But at type-erasure boundaries�virtual functions, library boundaries�concrete types are necessary.
Capy’s approach:
-
User-facing APIs � Accept concepts for maximum flexibility
-
Type-erasure boundaries � Use concrete spans internally
-
Library handles conversion � Users get concepts; implementation uses spans
This gives users the composition benefits of concepts while hiding the concrete types needed for virtual dispatch.
Why Not std::byte?
Even std::byte imposes a semantic opinion. POSIX uses void* for semantic neutrality�"raw memory, I move bytes without opining on contents."
But span<void> doesn’t compile�C++ can’t express type-agnostic buffer abstraction with span.
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.
Summary
The reflexive span<span<byte>> approach:
-
Forces allocation on every composition
-
Leads to overload proliferation
-
Loses the compositional power of generic programming
The concept-driven approach:
-
Enables zero-allocation composition
-
Provides a single signature that accepts anything buffer-like
-
Follows proven STL design principles
Continue to Buffer Types to learn about const_buffer and mutable_buffer.