C++ / a working model

83 / 163   ·   C++11   ·   14 min

streambuf layering: give formatted output a different destination

Keep this sentence

ostream turns values into characters; streambuf delivers the characters. Catch the same set of formatting operations with a fixed-capacity custom buffer, then force a definite capacity shortfall, and observe how a device-layer failure is propagated as the stream's badbit.

In this lesson
  1. Formatting and the device should not be bound together
  2. What happens after the put area is full
  3. Check the stream state, and also check writes that already happened
  4. Example
  5. Exercise
READING EVIDENCE / Partial text read

Standard C++ IOStreams and Locales: Advanced Programmer's Guide and Reference

Fully read the author's publicly released licensed excerpt of streambuf: The Stream Buffer Classes, marked as taken from printed pages 84–109, covering the hierarchy, get/put area, string/file buffers, and putback. This is a selected excerpt article and is not claimed to be a verbatim equivalent of the book's 26 pages. The remaining chapters 1–3, the locale portion, and the reference manual were not obtained and not read.

Edition, actual reading range, and original sources →

Formatting and the device should not be bound together

If the output destination changes from a file to memory, the rules for converting integers to text should not have to be rewritten. The key boundary shown in the excerpt is that the stream interprets format and state, while streambuf provides the transport interface for character sequences. Plug a custom device into the character layer and you can reuse ostream's formatting capabilities.

The sample device is a fixed-size array. It has no file, network, or dynamic allocation, does not support seeking, and only guarantees sequential writes from the start. Making the promised contract precise is more reliable than appearing to implement a complete device while missing edge cases; a fixed capacity also makes the failure path deterministically reproducible.

What happens after the put area is full

At construction, setp sets the start and one-past-the-end pointers so ordinary characters can go directly into the put area; when space is exhausted, further writes cannot continue. We do not silently grow a full buffer; instead we return traits_type::eof() to indicate that a character cannot be accepted.

overflow receives an int_type and must not convert it to char before testing for EOF, or it will confuse a character with the end-of-file marker. The sample returns not_eof for an EOF request, indicating that a request that needs no new character was handled successfully; for a genuine new character it returns EOF. This derived class has no data pending flush to another device; the character array itself is the final destination.

Check the stream state, and also check writes that already happened

A failed output does not mean previously written characters are automatically undone. The sample first writes three characters successfully, then appends a fourth, asserting that badbit is set and that the original three characters remain; this checks both the upper-layer state and the lower-layer content. A protocol that needs atomic commit of a whole message should add a separate transactional buffer; do not mistake ostream for a database.

The buffer object must outlive the ostream that refers to it. This example destroys in reverse declaration order: the stream first, then the buffer. The book's diagrams of file-buffer pointer arrangement are an explanatory model, not a mandated ABI; the layered interface still applies in C++20, but do not depend on whether a given standard-library implementation uses a single buffer or a double buffer internally.

Pitfalls

  • badbit does not undo characters already output; restoring stream state is also not the same as enlarging device capacity.
  • ostream does not own a custom streambuf passed to it; destroying the buffer first and then using the stream leaves a dangling pointer.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <array>
#include <cassert>
#include <cstddef>
#include <locale>
#include <ostream>
#include <streambuf>
#include <string>
class FixedBuffer : public std::streambuf {
    std::array<char, 3> data_{};
protected:
    int_type overflow(int_type c = traits_type::eof()) override {
        if (traits_type::eq_int_type(c, traits_type::eof()))
            return traits_type::not_eof(c);
        return traits_type::eof();
    }
public:
    FixedBuffer() { setp(data_.data(), data_.data() + data_.size()); }
    FixedBuffer(const FixedBuffer&) = delete;
    FixedBuffer& operator=(const FixedBuffer&) = delete;
    std::string text() const { return std::string(pbase(), pptr()); }
};
int main() {
    FixedBuffer buffer;
    std::ostream out(&buffer);
    out.imbue(std::locale::classic());
    out << 42 << '!';
    assert(out.good());
    assert(buffer.text() == "42!");
    out.put('?');
    assert(out.bad());
    assert(buffer.text() == "42!");
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-iostreams-locales.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

What happens if you change the capacity from 3 to 4, keep the first two writes, and then append one more character?

Show a reference answer

After changing the array to std::array<char, 4>, outputting 42! and then put(?) both succeed, with content 42!?; only a further put(#) sets badbit, and the content remains 42!?. The corresponding assertions should check good() after the fourth character and bad() after the fifth; this tests the capacity boundary, not the number of calls.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog