Contiguous Buffers

The most efficient way to provide storage for bytes is a contiguous region: an array. While it is not always possible to efficiently compose a full message in a single contiguous region, it is certainly a good default. Capy offers two types for this purpose:

  • const_buffer — a handle to a contiguous sequence of bytes. I/O operations will never modify these bytes.

  • mutable_buffer — a handle to a contiguous region for sttoring bytes; I/O operations will fill it.

Both types are present in header <boost/capy/buffers.hpp>.

Passing Handles

Objects of type const_buffer and mutable_buffer store a pointer to the beginning of a memory region and the size of the region measured in bytes. This is reflected in how they are constructed and how they are copied.

std::string_view header = "Content-Type: application/json";
capy::const_buffer buf(header.data(), header.size());  (1)
capy::const_buffer buf2 = buf;                         (2)

assert(buf.data() == header.data());
assert(buf2.data() == header.data());
1 You need to pass the pointer to the beginning of the region and its size.
2 Copies the pointer and the size.

As operations take the buffers by value, or store them by value as data members, only the handle is copied, while the location of the bytes does not change.

unit buffer

Unlike std::span, a single buffer type can represent storage for different trivially-copyable types:

char carray [1024];
capy::mutable_buffer buf1(carray, sizeof(carray));       (1)

std::array<std::byte, 1024> barray;
capy::mutable_buffer buf2(barray.data(), barray.size()); (2)

std::vector<int> iarray(256);
capy::mutable_buffer buf3(iarray.data(), iarray.size() * sizeof(int)); (3)
1 C array of char is fine.
2 Array of std::byte is fine.
3 Vector of int is fine: the integers will be interpreted as bytes during transmission.

Lifetime Management

The contract for read and write operations in Capy is:

  • The user manages the lifetime of the byte storage.

  • I/O operations receive a handle to the storage and assume that it will be available for the entire duration of the asynchronous operations.

char storage[1024];
capy::mutable_buffer buf(storage, sizeof(storage));
auto [ec, n] = co_await stream.read_some(buf);  (1)
process(storage, n);
1 OK: The read will be performed at an unpredictable time, but because afterwards the coroutine needs to be resumed, its frame (containing the array) is guaranteed to outlive the read operation.
char storage[1024];
capy::mutable_buffer buf(storage, sizeof(storage));
return stream.read_some(buf);  (1)
1 Bug: the awaitable, rather than being `co_await`ed, is returned to be used later. At that later time the array will no longer be alive.

Mutable versus Const Buffers

mutable_buffer represents the storage for bytes, intended to be written. The value of the bytes is irrelevant, because they will be overwritten. In fact, they may have indeterminate value.

const_buffer represents bytes to be read. It is not only storage, it is also values.

std::string_view header = "Content-Type: application/json";
capy::const_buffer buf(header.data(), header.size());
auto [ec, n] = co_await stream.read_some(buf);  (1)
1 Compiler error: passing const_buffer but the read operation needs to mutate its contents.

A mutable_buffer is convertible to const_buffer. However, you should be very careful about this, as this conversion allows using pure storage, possibly with indeterminate byte values, in functions that need to read the byte values:

char storage[1024];                               (1)
capy::mutable_buffer mbuf(storage, sizeof(storage));
auto [ec, n] = co_await stream.write_some(mbuf);  (2)
1 The storage starts with indeterminate byte values (intended to be overwritten).
2 Bug: will read indeterminate byte values, which is undefined behavior.

Buffer Processing Interface

Algorithms operating on buffers use the following interface provided by both const_buffer and mutable_buffer.

void buffer_interface(capy::const_buffer   cbuf, 
                      capy::mutable_buffer mbuf)
{
  std::size_t len  = cbuf.size();  (1)
  std::size_t mlen = mbuf.size();

  void const* ptr = cbuf.data();   (2)
  void*      mptr = mbuf.data();

  mbuf += len;                     (3)

  assert(mbuf.data() == (char*)mptr + len);
  assert(mbuf.size() == mlen - len);
}
1 The size of the buffer measured in bytes.
2 Pointer to the first byte of the buffer. It is const for const_buffer.
3 Shrink to represent a subrange of the original by omitting the initial len bytes.

make_buffer

There is a convenience factory function make_buffer that can create either a const_buffer or mutable_buffer from your type, as long as it is recognized as a contiguous storage of trivially-copyable types.

#include <boost/capy/buffers/make_buffer.hpp>
char arr[10];
capy::mutable_buffer buf = capy::make_buffer(arr);
std::array<std::byte, 10> std_arr;
capy::mutable_buffer buf = capy::make_buffer(std_arr);
std::vector<int> vec(100);
capy::mutable_buffer buf = capy::make_buffer(vec);
std::string str = "message";
capy::mutable_buffer buf = capy::make_buffer(str);
std::string_view str = "message";
capy::const_buffer buf = capy::make_buffer(str);
std::span<char> sp(arr); // or boost::span
capy::mutable_buffer buf = capy::make_buffer(sp);
std::array<std::array<char, 4>, 100> array2;
capy::mutable_buffer buf = capy::make_buffer(array2);
assert(buf.size() == 400);

The const-ness of .data() on the source type determines if the factory produces const_buffer or mutable_buffer.