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.

Prerequisites

  • Basic C++ experience with memory and pointers

  • Familiarity with C++20 concepts

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:

  1. Allocate a new buffer large enough for both

  2. Copy headers into the new buffer

  3. Copy body after headers

  4. 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.