Buffer Types

This section introduces Capy’s fundamental buffer types: const_buffer and mutable_buffer.

Buffers Are Handles

A const_buffer or mutable_buffer is a handle: a non-owning (pointer, size) view of memory it does not own. Constructing one copies no bytes, and destroying one frees nothing.

This splits lifetime responsibility cleanly:

  • You own the bytes. The memory a buffer refers to—a stack array, a std::string, a slab from your allocator—is yours to keep alive. It must remain valid for the entire duration of any operation you hand the buffer to. That includes the suspension points of a co_await-ed I/O operation.

  • The library owns the handles. Capy creates and manages buffer handles and handle-sequences on your behalf. Those handles are the buffers a dynamic buffer exposes through prepare/data and the sub-range a buffer_slice produces. They are also the descriptors a type-erased stream passes to the OS. Each such handle is valid only for the window its API documents, typically until the next call that mutates the owner.

The library never copies or takes ownership of your bytes through a buffer; it only moves handles. This split explains every buffer-lifetime rule in this chapter.

Why void*, Not std::byte?

std::byte imposes a semantic opinion. It says "this is raw bytes"—but that is itself an opinion about the data’s nature.

POSIX uses void* for buffers. This expresses semantic neutrality: "I move memory without opining on what it contains." The OS doesn’t care if the bytes represent text, integers, or compressed data—it moves them.

Two concrete forces favor void* specifically over std::span<std::byte>:

  • Platform types already use it. The OS structures Capy maps onto—iovec’s `iov_base, WSABUF’s `buf—are void*/char*, so the pointer field never needs reinterpreting. Only iovec matches Capy’s full struct layout; `WSABUF’s field order and width differ, as the Layout Compatibility section below explains.

  • Callers supply many element types. User data arrives as char[], unsigned char[], std::byte[], std::string, and more. A single neutral pointer erases all of them to one representation. std::span<std::byte> would force every caller to reinterpret their bytes first, and std::span<void> is ill-formed—C++ cannot express a type-agnostic buffer with span.

Capy provides const_buffer and mutable_buffer as semantically neutral buffer types with known layout.

const_buffer

const_buffer represents a contiguous region of read-only memory. See const_buffer for the full interface.

Construction

// From pointer and size
char data[] = "hello";
const_buffer buf(data, 5);

// From mutable_buffer (implicit)
mutable_buffer mbuf(data, 5);
const_buffer cbuf = mbuf;  // OK: mutable -> const

Accessors

const_buffer buf(data, 5);

void const* ptr = buf.data();  // Pointer to first byte
std::size_t len = buf.size();  // Number of bytes

Prefix Removal

The += operator removes bytes from the front of the buffer:

const_buffer buf(data, 10);

buf += 3;  // Remove first 3 bytes
// buf.data() now points 3 bytes later
// buf.size() is now 7

This is useful when processing a buffer incrementally.

mutable_buffer

mutable_buffer represents a contiguous region of writable memory. See mutable_buffer for the full interface.

The interface mirrors const_buffer, but data() returns non-const void*.

Conversion

mutable_buffer implicitly converts to const_buffer:

void process(const_buffer buf);

mutable_buffer mbuf(data, size);
process(mbuf);  // OK: implicit conversion

The reverse is not allowed—you cannot implicitly convert const_buffer to mutable_buffer.

make_buffer

The make_buffer function creates buffers from various sources:

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

// From pointer and size
auto buf = make_buffer(ptr, size);

// From C array
char arr[10];
auto arr_buf = make_buffer(arr);

// From std::array
std::array<char, 10> std_arr;
auto std_arr_buf = make_buffer(std_arr);

// From std::vector
std::vector<char> vec(100);
auto vec_buf = make_buffer(vec);

// From std::string
std::string str = "hello";
auto str_buf = make_buffer(str);

// From std::string_view
std::string_view sv = "hello";
auto sv_buf = make_buffer(sv);

// From a span (std::span or boost::span)
std::span<char> sp(arr);
auto sp_buf = make_buffer(sp);

make_buffer accepts any sized, contiguous range of trivially-copyable elements—including std::span and boost::span—in addition to the sources shown above.

The returned buffer type depends on the element constness of the range:

The buffer’s size, in bytes, is count * sizeof(element).

Layout Compatibility

const_buffer and mutable_buffer place the pointer first and the size second:

  • POSIX: struct iovec { void* iov_base; size_t iov_len; } — same order, same widths. An array of buffers converts to iovec with a direct reinterpret_cast.

  • Windows: struct WSABUF { ULONG len; CHAR* buf; } — reversed order, and a 32-bit length instead of size_t. Capy copies descriptors into a WSABUF array instead of casting; see System I/O Integration.

Single Buffers as Sequences

A single buffer is a degenerate sequence—a sequence with one element. The ConstBufferSequence and MutableBufferSequence concepts recognize this:

template<ConstBufferSequence Buffers>
void write_data(Buffers const& buffers);

// All of these work:
write_data(make_buffer("hello"));         // Single buffer
write_data(std::array{buf1, buf2, buf3}); // Multiple buffers
write_data(my_composite);                 // Custom sequence

The library provides begin() and end() functions that work uniformly:

const_buffer single;
auto it = begin(single);  // Returns pointer to single
auto e = end(single);     // Returns pointer past single

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