Dynamic Buffers
This page explains dynamic buffers: containers that manage readable and writable byte regions with automatic sizing. After reading, you will understand the prepare/commit/consume workflow and know when to choose each buffer type.
| Code snippets on this page assume the following declarations are in effect: |
#include <boost/buffers.hpp>
using namespace boost::buffers;
What Are Dynamic Buffers?
Static buffer sequences represent fixed memory regions. Dynamic buffers add the ability to grow and shrink, making them suitable for streaming data where the final size is unknown.
A dynamic buffer divides its storage into two logical regions:
-
Input sequence - Contains readable data (accessed via
data()) -
Output sequence - Contains writable space (accessed via
prepare())
Data flows from output to input through explicit operations. The commit(n)
operation moves bytes from the output sequence to the input sequence. The
consume(n) operation removes bytes from the input sequence.
The Dynamic Buffer Workflow
Working with dynamic buffers follows a consistent pattern:
-
Prepare - Request writable space with
prepare(n) -
Write - Fill the output sequence with data
-
Commit - Move written bytes to the input sequence with
commit(n) -
Read - Access the input sequence with
data() -
Consume - Remove processed bytes with
consume(n)
Example: Accumulating Data
char storage[1024];
circular_buffer cb(storage, sizeof(storage));
// Write "Hello"
{
auto dest = cb.prepare(5);
copy(dest, const_buffer("Hello", 5));
cb.commit(5);
}
// Write ", World!"
{
auto dest = cb.prepare(8);
copy(dest, const_buffer(", World!", 8));
cb.commit(8);
}
// Read accumulated data
auto readable = cb.data();
// size(readable) == 13
// Contains "Hello, World!"
// Consume the greeting
cb.consume(7); // Remove "Hello, "
// size(cb.data()) == 6
// Contains "World!"
Buffer Types
The library provides three dynamic buffer implementations, each with different trade-offs:
| Type | Storage | Best For |
|---|---|---|
Fixed external array |
Network I/O with bounded memory |
|
Fixed external array |
Protocols requiring contiguous input |
|
Growing |
Building string output dynamically |
circular_buffer
A circular_buffer uses a fixed-size external array as a ring buffer. The
readable and writable regions wrap around the array boundaries, enabling
efficient use of bounded memory for streaming data.
Construction
char storage[4096];
// Empty buffer with full capacity
circular_buffer cb1(storage, sizeof(storage));
// Buffer with initial readable content
circular_buffer cb2(storage, sizeof(storage), 100);
// cb2.size() == 100 (readable bytes)
// cb2.capacity() == 3996 (writable bytes)
Key Properties
-
Fixed capacity - The buffer never allocates;
max_size()equals the array size -
Two-element sequences - Both
data()andprepare()returnconst_buffer_pairormutable_buffer_pair, since the data may wrap around the array boundary -
Efficient memory reuse - Consumed space becomes available for writing without copying
Buffer Pairs and Wrapping
When data wraps around the array boundary, both the input and output sequences consist of two separate memory regions:
char storage[10];
circular_buffer cb(storage, sizeof(storage));
// Fill most of the buffer
auto dest = cb.prepare(8);
copy(dest, const_buffer("XXXXXXXX", 8));
cb.commit(8);
// Consume from the front
cb.consume(6);
// Now write more - this wraps around
dest = cb.prepare(6);
// dest[0] points to bytes 8-9 (end of array)
// dest[1] points to bytes 0-3 (wrapped to start)
The copy function handles buffer pairs transparently, so wrapping is
usually invisible to the caller.
flat_buffer
A flat_buffer uses a fixed-size external array but guarantees that the
input sequence is always contiguous. This simplifies protocols that require
parsing contiguous data.
Construction
char storage[4096];
// Empty buffer
flat_buffer fb1(storage, sizeof(storage));
// With initial content
flat_buffer fb2(storage, sizeof(storage), 100);
Key Properties
-
Contiguous input - The
data()method returns a singleconst_buffer, never a pair -
Contiguous output - The
prepare()method returns a singlemutable_buffer -
Internal shifting - When the buffer runs low on writable space, data may be shifted to reclaim consumed space
-
Single-buffer sequences - Simpler to use with APIs expecting contiguous data
Example: Parsing Contiguous Data
char storage[1024];
flat_buffer fb(storage, sizeof(storage));
// Accumulate incoming data
auto dest = fb.prepare(100);
std::size_t n = read_some(socket, dest);
fb.commit(n);
// Parse - input is guaranteed contiguous
auto input = fb.data();
char const* p = static_cast<char const*>(input.data());
std::size_t len = input.size();
// Process contiguous data directly
auto result = parse_message(p, len);
fb.consume(result.bytes_consumed);
string_buffer
A string_buffer wraps a std::string, allowing the buffer to grow
dynamically. Unlike the fixed-capacity buffers, string_buffer can allocate
memory as needed.
Construction
std::string storage;
// Wrap an existing string
string_buffer sb1(&storage);
// With maximum size limit
string_buffer sb2(&storage, 10000);
The buffer takes a pointer to the string, which must outlive the buffer object. On destruction, the buffer resizes the string to match the input sequence size.
Key Properties
-
Growing capacity - The buffer allocates via the string’s allocator
-
Move-only - Cannot be copied; can be moved
-
Automatic finalization - Destructor sets the string’s size to the readable byte count
-
Single-buffer sequences - Like
flat_buffer, returns contiguous buffers
Example: Building Output
std::string result;
{
string_buffer sb(&result);
// Write header
auto dest = sb.prepare(100);
std::size_t n = format_header(dest.data(), dest.size());
sb.commit(n);
// Write body
dest = sb.prepare(1000);
n = format_body(dest.data(), dest.size());
sb.commit(n);
} // Destructor finalizes result.size()
// result now contains the formatted output
send(socket, result.data(), result.size());
Choosing a Buffer Type
| Requirement | circular_buffer | flat_buffer | string_buffer |
|---|---|---|---|
Fixed memory |
Yes |
Yes |
No |
Contiguous input |
No |
Yes |
Yes |
Growing capacity |
No |
No |
Yes |
Memory efficiency |
Excellent |
Good |
Varies |
Parsing simplicity |
Moderate |
Simple |
Simple |
Use circular_buffer when you have bounded memory and can handle
non-contiguous data. This is the most memory-efficient choice for streaming.
Use flat_buffer when you need contiguous data for parsing but have
predictable memory requirements.
Use string_buffer when building output strings or when the data size is
truly unpredictable and allocation is acceptable.
Error Handling
Dynamic buffers throw exceptions in these cases:
-
prepare(n)exceeds capacity - Throwsstd::length_errorifsize() + n > max_size() -
Invalid construction - Throws
std::invalid_argumentif initial size exceeds capacity
When using -fno-exceptions, these conditions call std::abort().
char storage[100];
circular_buffer cb(storage, sizeof(storage));
// Fill to capacity
cb.prepare(100);
cb.commit(100);
// This throws - no space available
try {
cb.prepare(1); // throws std::length_error
} catch(std::length_error const& e) {
// Handle overflow
}