Buffer Sequences
This section explains buffer sequences—the concept that enables zero-allocation composition of buffers.
Prerequisites
-
Completed Buffer Types
-
Understanding of
const_bufferandmutable_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_bufferdirectly (single buffer), OR -
It is a bidirectional range whose elements convert to
const_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 toread_some/write_some) -
consume(n)— Advance pastntransferred bytes, in place
Why Bidirectional?
The concepts require bidirectional ranges (not just forward ranges) for two reasons:
-
Some algorithms traverse buffers backwards
-
The slice views produced by
buffer_sliceandconsuming_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 |
|---|---|
|
Concepts and iteration functions |
|
Byte sub-range slicing algorithm |
|
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.