Dynamic Buffers
This section introduces dynamic buffers—growable storage that adapts to data flow between producers and consumers.
Prerequisites
-
Completed Buffer Algorithms
-
Understanding of buffer sequences and copying
The Producer/Consumer Model
Dynamic buffers serve as intermediate storage between a producer (typically network I/O) and a consumer (your application code).
The flow:
-
Producer writes data into the buffer
-
Buffer grows as needed to accommodate data
-
Consumer reads and processes data
-
Buffer releases consumed data
This model decouples production rate from consumption rate—the buffer absorbs variations.
The DynamicBuffer Concept
template<typename T>
concept DynamicBuffer = requires(T& t, std::size_t n) {
typename T::const_buffers_type;
typename T::mutable_buffers_type;
// Producer side
{ t.prepare(n) } -> std::same_as<typename T::mutable_buffers_type>;
{ t.commit(n) };
// Consumer side
{ t.data() } -> std::same_as<typename T::const_buffers_type>;
{ t.consume(n) };
// Capacity
{ t.size() } -> std::same_as<std::size_t>;
{ t.max_size() } -> std::same_as<std::size_t>;
{ t.capacity() } -> std::same_as<std::size_t>;
};
A dynamic buffer, within its capacity provides a potentially empty readable region and a potentially empty writeable region. Sizes of these regions change as operations are invoked on the dynamic buffer.
Producer Interface
prepare(n)
Returns a mutable buffer sequence mb for writing up to n bytes:
capy::MutableBufferSequence auto buffers = dynamic_buf.prepare(1024); // Space for up to 1024 bytes
Effects: If n > max_size() - size() throws std::length_error().
Otherwise, ensures that a wirteable region of size at leats n exists
and returns an object of type mutable_buffers_type representing this region.
Throws: std::bad_alloc in case an allocation is performed and fails.
Postcondition: buffer_size(buffers) >= n.
commit(n)
Transfers n bytes of from the beginning of the writeable storage to
the end of the readable storage:
// After writing data:
dynamic_buf.commit(bytes_written);
// Data is now visible via data()
Let n1 be the smaller number of n and the size of the writeable region.
Effects: Removes n1 bytes from the front of the writeable region and
adds them at the back of the readable region.
Notes: n can be smaller than the writeable region size.
Consumer Interface
data()
Returns a const buffer sequence representing the readable region.
capy::ReadableBufferSequence auto readable = dynamic_buf.data();
// Process readable bytes
Postcondition: capy::buffer_size(readable) == dynamic_buf.size().
consume(n)
Removes n bytes from the front of readable data:
dynamic_buf.consume(processed_bytes);
// Those bytes are no longer in data()
Let n1 be the smaller of n and size().
Effects: Removes n1 bytes from the front of the readable region.
Sets the size of the writeable rgion to zero. Invalidates all buffer
sequences previously obtained via calls to data() or `prepare().
Typical Consumer Pattern
void process_buffer(DynamicBuffer auto& buffer)
{
auto data = buffer.data();
while (buffer_size(data) >= message_header_size)
{
auto msg_size = parse_header(data);
if (buffer_size(data) < msg_size)
break; // Need more data
process_message(data, msg_size);
buffer.consume(msg_size);
data = buffer.data(); // Refresh after consume
}
}
DynamicBufferParam
When passing dynamic buffers to coroutines, use DynamicBufferParam for safe parameter handling:
template<typename DB>
concept DynamicBufferParam = DynamicBuffer<std::remove_reference_t<DB>>;
template<DynamicBufferParam Buf>
task<std::size_t> read_until(Stream& stream, Buf&& buffer, char delimiter);
This concept ensures proper handling of lvalues and rvalues, preventing dangling references across suspension points.
Provided Implementations
flat_dynamic_buffer
Linear storage with single-buffer sequences:
#include <boost/capy/buffers/flat_dynamic_buffer.hpp>
flat_dynamic_buffer buffer;
buffer.prepare(1024);
// ... write data ...
buffer.commit(n);
// data() returns a single const_buffer
Advantages:
-
Contiguous memory—good for parsing that needs contiguous data
-
Cache-friendly
Disadvantages:
-
May require copying when buffer wraps or grows
circular_dynamic_buffer
Ring buffer implementation:
#include <boost/capy/buffers/circular_dynamic_buffer.hpp>
circular_dynamic_buffer<1024> buffer; // Fixed capacity
Advantages:
-
No copying on wrap—head/tail pointers move
-
Fixed memory footprint
Disadvantages:
-
data()may return two buffers (wrapped around end) -
Fixed capacity
Example: Line-Based Protocol
task<std::string> read_line(Stream& stream)
{
flat_dynamic_buffer buffer;
while (true)
{
// Prepare space and read
auto space = buffer.prepare(256);
auto [ec, n] = co_await stream.read_some(space);
buffer.commit(n);
if (ec)
throw std::system_error(ec);
// Search for newline in readable data
auto data = buffer.data();
std::string_view sv(
static_cast<char const*>(data.data()), data.size());
auto pos = sv.find('\n');
if (pos != std::string_view::npos)
{
std::string line(sv.substr(0, pos));
buffer.consume(pos + 1); // Include newline
co_return line;
}
}
}
Reference
| Header | Description |
|---|---|
|
DynamicBuffer concept definition |
|
Linear dynamic buffer |
|
Ring buffer implementation |
|
Vector-backed adapter |
|
String-backed adapter |
You have now learned about dynamic buffers for producer/consumer patterns. This completes the Buffer Sequences section. Continue to Stream Concepts to learn about Capy’s stream abstractions.