Buffer Algorithms
Counting Bytes
buffer_size returns the total number of bytes across every buffer in a sequence:
auto buf1 = capy::make_buffer("hello"sv); // 5 bytes
auto buf2 = capy::make_buffer("world"sv); // 5 bytes
auto buf3 = capy::make_buffer(""sv); // 0 bytes
auto combined = std::array{buf1, buf2, buf3};
assert(capy::buffer_size(combined) == 10);
buffer_empty reports whether a sequence carries no data,
either because it holds no buffers or because every buffer has size zero.
It is a shorthand for buffer_size(bufseq) == 0.
Capy allows passing zero-size buffers to read and write operations. This is to cover the situations where the size of the buffer is determined dynamically, based on incoming messages. A protocol may require that a sender first communicates the size of a message, and then the bytes of the message. In such protocols, having nothing to send may be correctly represented by sending the number zero, followed by no bytes. This would translate into later using a zero-size buffer.
|
The following two functions have similar names but return different values:
A three-buffer sequence of 100 bytes each has a |
buffer_slice
buffer_slice returns a byte sub-range of a buffer sequence, as a value:
#include <boost/capy/buffers/buffer_slice.hpp>
// send only the first 16 KB
co_await capy::write(stream, capy::buffer_slice(bufs, 0, 16384));
// everything after the first 16 KB
auto rest = capy::buffer_slice(bufs, 16384);
co_await capy::write(stream, rest);
buffer_slice(seq, offset, length) returns a value that is itself a buffer sequence, so you can pass it to any operation expecting one. Both offset and length are optional, which makes it a general byte sub-range primitive. Except in the single-buffer case the result borrows seq, so the sequence must outlive the slice.
Capy creates and manages buffer handles on your behalf. Those are the sub-range a buffer_slice produces, and the descriptors a type-erased stream passes to the OS. Each is valid only for the window its API documents.
consuming_buffers
When transferring data incrementally, consuming_buffers is a cursor that tracks progress:
#include <boost/capy/buffers/consuming_buffers.hpp>
template<capy::MutableBufferSequence Buffers>
capy::task<std::size_t> read_all(Stream& stream, Buffers buffers)
{
capy::consuming_buffers consuming(buffers);
std::size_t const total_size = capy::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;
}
The cursor borrows the underlying sequence and provides:
|
A coroutine reads its parameters when its body runs, not when the call is written. A task that is stored and awaited later outlives its call expression, so a reference parameter can dangle by then. That is why the fan-out example on Concurrent Composition takes its item by value: it collects its tasks first, then awaits them together. |
Copying
buffer_copy copies data from one buffer sequence to another and returns the number of bytes copied:
char source_data[] = "hello world";
char dest_data[20];
capy::const_buffer src(source_data, 11);
capy::mutable_buffer dst(dest_data, 20);
std::size_t copied = capy::buffer_copy(dst, src); // 11
Its at_most parameter caps the transfer, which is useful for protocols with size limits:
// Copy at most 5 bytes
std::size_t copied = capy::buffer_copy(dst, src, 5);
Source and target need not have the same shape:
// Source: 3 buffers
std::array<capy::const_buffer, 3> src = {buf1, buf2, buf3};
// Target: 2 buffers with different sizes
std::array<capy::mutable_buffer, 2> dst = {large_buf, small_buf};
// Copies across buffer boundaries as needed
std::size_t copied = capy::buffer_copy(dst, src);
The algorithm fills target buffers in order, reading from source buffers as needed. It handles a source buffer spanning several targets, and the reverse.
Partial Transfer Loops
Real transfers move some of the bytes, not all of them. Drive the loop with a consuming_buffers cursor, described in consuming_buffers.
template<capy::ReadStream Stream, capy::MutableBufferSequence Buffers>
capy::task<std::size_t> read_full(Stream& stream, Buffers buffers)
{
capy::consuming_buffers remaining(buffers);
std::size_t const total_size = capy::buffer_size(buffers);
std::size_t total = 0;
while (total < total_size)
{
auto [ec, n] = co_await stream.read_some(remaining.data());
remaining.consume(n);
total += n;
if (ec)
co_return total;
}
co_return total;
}
template<capy::WriteStream Stream, capy::ConstBufferSequence Buffers>
capy::task<std::size_t> write_full(Stream& stream, Buffers buffers)
{
capy::consuming_buffers remaining(buffers);
std::size_t const total_size = capy::buffer_size(buffers);
std::size_t total = 0;
while (total < total_size)
{
auto [ec, n] = co_await stream.write_some(remaining.data());
remaining.consume(n);
total += n;
if (ec)
co_return total;
}
co_return total;
}
Custom Buffer Types
Any memory region can be a buffer, including a memory-mapped one:
// Memory-mapped file
void* mapped = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
capy::const_buffer file_buf(mapped, file_size);
co_await capy::write(socket, file_buf); // Zero-copy network transmission
You can also define your own type satisfying the concepts:
class chunked_buffer_sequence
{
std::vector<std::vector<char>> chunks_;
public:
auto begin() const { return chunk_iterator(chunks_.begin()); }
auto end() const { return chunk_iterator(chunks_.end()); }
};
// Satisfies ConstBufferSequence—works with all algorithms
Here chunk_iterator is a small bidirectional iterator whose operator* returns each chunk as a const_buffer.