Buffer Sequences

This section explains buffer sequences—the concept that enables zero-allocation composition of buffers.

Prerequisites

  • Completed Buffer Types

  • Understanding of const_buffer and mutable_buffer

What Is a Buffer Sequence?

A buffer sequence is any type that can produce an iteration of buffers. Formally:

  • A single buffer (like const_buffer) is a sequence of one element

  • A range of buffers (like vector<const_buffer>) is a multi-element sequence

  • Any bidirectional range with buffer-convertible values qualifies

Treating a single buffer as a one-element sequence is a deliberate convenience, not an accident of the definition. It lets one concept-constrained signature serve both the common single-buffer call and scatter/gather composition, with no overload and no explicit wrap at the call site. Capy favors this convenience as a primary design goal and applies it consistently—make_buffer, for instance, accepts any contiguous range of bytes—so that buffer-passing reads the same whether you hand over one region or many.

The Concepts

ConstBufferSequence

template<typename T>
concept ConstBufferSequence =
    std::is_convertible_v<T, const_buffer> || (
        std::ranges::bidirectional_range<T> &&
        std::is_convertible_v<std::ranges::range_value_t<T>, const_buffer>);

A type satisfies ConstBufferSequence if:

  • It converts to const_buffer directly (single buffer), OR

  • It is a bidirectional range whose elements convert to const_buffer

MutableBufferSequence

template<typename T>
concept MutableBufferSequence =
    std::is_convertible_v<T, mutable_buffer> || (
        std::ranges::bidirectional_range<T> &&
        std::is_convertible_v<std::ranges::range_value_t<T>, mutable_buffer>);

Same pattern, but for mutable_buffer.

Satisfying the Concepts

Many common types satisfy these concepts:

// Single buffers
const_buffer cb;                    // ConstBufferSequence
mutable_buffer mb;                  // MutableBufferSequence (and ConstBufferSequence)

// Standard containers of buffers
std::vector<const_buffer> v;        // ConstBufferSequence
std::array<mutable_buffer, 3> a;    // MutableBufferSequence

// String types (wrap with make_buffer to get a single buffer)
std::string str;                    // make_buffer(str) -> mutable_buffer
std::string_view sv;                // make_buffer(sv) -> const_buffer

Note that std::string and std::string_view are ranges of characters, not of buffers, so they do not satisfy the concepts themselves; wrap them with make_buffer to obtain a single-buffer sequence.

Heterogeneous Composition

Because the concept accepts anything convertible to buffer, you can mix types:

template<ConstBufferSequence Buffers>
void send(Buffers const& bufs);

// All of these work:
send(make_buffer("Hello"));                    // string literal
send(make_buffer(std::string_view{"Hello"}));  // string_view
send(std::array{buf1, buf2});                  // array of buffers
send(my_custom_buffer_sequence);               // custom type

Iterating Buffer Sequences

Use begin() and end() from <boost/capy/buffers.hpp>:

template<ConstBufferSequence Buffers>
void process(Buffers const& bufs)
{
    for (auto it = begin(bufs); it != end(bufs); ++it)
    {
        const_buffer buf = *it;
        // Process buf.data(), buf.size()
    }
}

These functions handle both single buffers (returning pointer-to-self) and ranges (returning standard iterators).

buffer_slice

buffer_slice returns a byte sub-range of a buffer sequence, as a value:

#include <boost/capy/buffers/buffer_slice.hpp>

co_await write(stream, buffer_slice(bufs, 0, 16384));  // send only the first 16 KB
auto rest = buffer_slice(bufs, 16384);                 // everything after the first 16 KB
co_await write(stream, rest);

buffer_slice(seq, offset, length) returns a value that is itself a buffer sequence: pass it directly to any operation expecting one. The offset and length parameters (both optional) make buffer_slice a general byte sub-range primitive. Except for the single-buffer case, the result borrows seq, so the sequence must outlive the slice.

consuming_buffers

When transferring data incrementally, consuming_buffers is a cursor that tracks progress:

#include <boost/capy/buffers/consuming_buffers.hpp>

template<MutableBufferSequence Buffers>
task<std::size_t> read_all(Stream& stream, Buffers buffers)
{
    consuming_buffers consuming(buffers);
    std::size_t const total_size = buffer_size(buffers);
    std::size_t total = 0;

    while (total < total_size)
    {
        auto [ec, n] = co_await stream.read_some(consuming.data());
        consuming.consume(n);
        total += n;
        if (ec)
            break;
    }

    co_return total;
}

A consuming_buffers cursor borrows the underlying sequence and provides:

  • data() — Buffer sequence view of the remaining bytes (pass to read_some/write_some)

  • consume(n) — Advance past n transferred bytes, in place

Why Bidirectional?

The concepts require bidirectional ranges (not just forward ranges) for two reasons:

  1. Some algorithms traverse buffers backwards

  2. The slice views produced by buffer_slice and consuming_buffers::data() need to adjust the first and last buffers' bounds

If your custom buffer sequence only provides forward iteration, wrap it in a type that provides bidirectional access.

Reference

Header Description

<boost/capy/buffers.hpp>

Concepts and iteration functions

<boost/capy/buffers/buffer_slice.hpp>

Byte sub-range slicing algorithm

<boost/capy/buffers/consuming_buffers.hpp>

Incremental consumption cursor

You have now learned how buffer sequences enable zero-allocation composition. Continue to System I/O Integration to see how buffer sequences interface with operating system I/O.