Composite Buffers

In real protocols, a message to be transmitted is often composed of a couple of chunks prepared separately in separate contiguous regions of storage. For instance, you may separately get message headers, the payload, and the message checksum. Trying to get them into a single contiguous region would require the user to do unnecessary copying.

In order to address this use case, all read and write operations in Capy allow you to provide ranges of contiguous buffers:

const std::string headers = make_headers();
const std::string body = make_body();
const std::string checksum = stamp(headers, body);

std::array buf {capy::make_buffer(headers), 
                capy::make_buffer(body), 
                capy::make_buffer(checksum)};

co_await stream.write_some(buf);

Buffer Sequences

In order to address both the simple case of a single contiguous buffer, and the complex cases when the data is produced in a number of contiguous buffers, Capy introduces a concept of a buffer sequence:

  • A contiguous buffer is a buffer sequence.

  • Any bidirectional range over contiguous buffers is a buffer sequence.

This generalizes the notion of a handle:

composite buffer

In the composite case, the entire range mechanism becomes a handle; but since the elements are const_buffer or mutable_buffer, the contiguous regions of memory they point to are not part of the handle. This still allows the algorithms to copy the handle while retaining the storage at its original location.

Treating a single contiguous buffer as a one-element sequence is intentional, to make the library API smaller. Internally, it is treated as a single-element range.

While a structure looks two-dimensional, the bytes across all contiguous buffers are interpreted and transmitted as a single sequence: first the bytes from the first contiguous buffer, then the bytes from the second contiguous buffer, and so on.

The mechanism for handling ranges of buffers assumes that the number of elements will be around four. In fact, some implementations will perform suboptimally when you use ranges of more than 16 contiguous buffers.

Serializing more buffers in one transmission would not be efficient, and you might as well merge the contents into a single buffer.

The Concepts

Concept MutableBufferSequence tests if the type models a mutable buffer sequence, suitable for read algorithms. It can be one of:

static_assert(capy::MutableBufferSequence<capy::mutable_buffer>);
static_assert(capy::MutableBufferSequence<std::span<capy::mutable_buffer>>);
static_assert(capy::MutableBufferSequence<std::vector<capy::mutable_buffer>>);
static_assert(capy::MutableBufferSequence<std::array<capy::mutable_buffer, 4>>);

Concept ConstBufferSequence tests if the type models a const buffer sequence, suitable for write algorithms. It can be one of:

static_assert(capy::ConstBufferSequence<capy::const_buffer>);
static_assert(capy::ConstBufferSequence<capy::mutable_buffer>);
static_assert(capy::ConstBufferSequence<std::span<capy::const_buffer>>);
static_assert(capy::ConstBufferSequence<std::span<capy::mutable_buffer>>);
static_assert(capy::ConstBufferSequence<std::array<capy::const_buffer, 4>>);
static_assert(capy::ConstBufferSequence<std::array<capy::mutable_buffer, 4>>);

These concepts are present in header <boost/capy/buffers.hpp>.

Navigating Buffer Sequences

Header <boost/capy/buffers.hpp> also provides a couple of function objects for navigating buffer sequences.

In order to iterate over contiguous buffers themselves (as opposed to bytes), use capy::begin and capy::end:

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

These functions treat single contiguous buffers as a one-element range of contiguous buffers:

capy::const_buffer single;
auto it = capy::begin(single);  // Returns pointer to `single`
auto e = capy::end(single);     // Returns pointer past `single`
assert(std::distance(it, e) == 1);

std::array<capy::const_buffer, 3> multi;
auto it2 = capy::begin(multi);  // Returns multi.begin()
auto e2 = capy::end(multi);     // Returns multi.end()
assert(std::distance(it2, e2) == 3);

Function buffer_length says how many contiguous buffers are present in the buffer sequence. It is equivalent to

std::distance(capy::begin(buf), capy::end(buf))