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.