Skip to content
C++
Library
since C++98
Advanced

std::streambuf

Abstract base class for C++ stream buffers, mediating raw character I/O between higher-level streams and the underlying data device.

std::basic_streambufsince C++98

The abstract intermediary between high-level stream objects and raw character sources or sinks, managing get and put buffer areas and defining a virtual interface for derived classes to specialize.

Overview

std::basic_streambuf<CharT, Traits> is the backbone of the C++ I/O architecture. Every stream object β€” std::istream, std::ostream, std::fstream, std::stringstream β€” delegates actual character movement to a basic_streambuf subclass. Two aliases cover the common cases:

cpp
using std::streambuf  = std::basic_streambuf<char>;    // C++98
using std::wstreambuf = std::basic_streambuf<wchar_t>; // C++98

The concrete subclasses bundled with the standard library are:

  • std::filebuf / std::wfilebuf β€” backs fstream objects; defined in <fstream>
  • std::stringbuf / std::wstringbuf β€” backs stringstream objects; defined in <sstream>

basic_streambuf manages two contiguous character arrays called the get area (input side) and the put area (output side). Pointers into these arrays are accessible via protected member functions:

AreaBeginCurrentEnd
Geteback()gptr()egptr()
Putpbase()pptr()epptr()

When a read exhausts the get area, the stream machinery calls underflow(). When a write fills the put area, it calls overflow(). Subclassing basic_streambuf means overriding these virtuals; everything else follows. This two-pointer model allows the buffer to track available-but-unread characters without copying them, which is fundamental to the performance of stream I/O.

The public API uses pub-prefixed forwarding functions (pubsync(), pubseekoff(), etc.) that call the protected virtuals. Never call the virtual functions directly from user code β€” always go through the public interface or through a stream object.

Syntax

cpp
#include <streambuf>

template<class CharT, class Traits = std::char_traits<CharT>>
class basic_streambuf {
public:
    // Locales
    std::locale pubimbue(const std::locale&);
    std::locale getloc() const;

    // Positioning
    pos_type pubseekoff(off_type, std::ios_base::seekdir,
                        std::ios_base::openmode = std::ios_base::in | std::ios_base::out);
    pos_type pubseekpos(pos_type,
                        std::ios_base::openmode = std::ios_base::in | std::ios_base::out);
    int pubsync();

    // Get area
    std::streamsize in_avail();
    int_type snextc();
    int_type sbumpc();
    int_type sgetc();
    std::streamsize sgetn(char_type* s, std::streamsize n);

    // Putback
    int_type sputbackc(char_type c);
    int_type sungetc();

    // Put area
    int_type sputc(char_type c);
    std::streamsize sputn(const char_type* s, std::streamsize n);

protected:
    // Customization-point virtuals
    virtual int_type  underflow();            // fill get area; do NOT advance gptr
    virtual int_type  uflow();               // fill get area; advance gptr
    virtual int_type  overflow(int_type c);  // flush put area; write c
    virtual int       sync();                // flush to underlying device
    virtual pos_type  seekoff(off_type, std::ios_base::seekdir, std::ios_base::openmode);
    virtual pos_type  seekpos(pos_type, std::ios_base::openmode);
    virtual std::streamsize showmanyc();
    virtual std::streamsize xsgetn(char_type* s, std::streamsize n);
    virtual std::streamsize xsputn(const char_type* s, std::streamsize n);

    // Buffer setup
    void setg(char_type* gbeg, char_type* gcurr, char_type* gend);
    void setp(char_type* pbeg, char_type* pend);
};

Examples

Stream redirection via rdbuf()

Every stream exposes rdbuf() as both a getter and a setter. This is the standard mechanism for redirecting std::cout, capturing output in tests, or temporarily routing a stream to a file:

cpp
#include <fstream>
#include <iostream>
#include <sstream>

// Capture std::cout into a string β€” useful in unit tests
void demo_capture() {
    std::ostringstream oss;
    std::streambuf* saved = std::cout.rdbuf(oss.rdbuf());  // redirect

    std::cout << "captured line\n";

    std::cout.rdbuf(saved);  // always restore before oss goes out of scope
    std::string result = oss.str();  // "captured line\n"
}

// Redirect cout to a file for a block
void demo_file_redirect() {
    std::ofstream log("run.log");
    std::streambuf* saved = std::cout.rdbuf(log.rdbuf());

    std::cout << "this goes to run.log\n";

    std::cout.rdbuf(saved);
}

Failing to restore the original buffer leaves the stream with a dangling pointer when the replacement buffer is destroyed β€” a silent use-after-free.

Custom streambuf: fan-out (tee) output

Subclassing basic_streambuf lets you intercept or duplicate character traffic without touching the higher-level stream API. The minimal contract for an output-only buffer is to override overflow() (single character) and xsputn() (bulk write):

cpp
#include <streambuf>
#include <ostream>
#include <algorithm>

class tee_buf : public std::streambuf {
public:
    tee_buf(std::streambuf* a, std::streambuf* b) : a_(a), b_(b) {}

protected:
    int_type overflow(int_type c) override {
        if (traits_type::eq_int_type(c, traits_type::eof()))
            return traits_type::not_eof(c);  // propagate EOF signal without error
        char_type ch = traits_type::to_char_type(c);
        if (traits_type::eq_int_type(a_->sputc(ch), traits_type::eof()))
            return traits_type::eof();
        return b_->sputc(ch);  // return eof() only on failure
    }

    std::streamsize xsputn(const char_type* s, std::streamsize n) override {
        std::streamsize wa = a_->sputn(s, n);
        std::streamsize wb = b_->sputn(s, n);
        return std::min(wa, wb);
    }

private:
    std::streambuf* a_;
    std::streambuf* b_;
};

// Usage:
//   std::ofstream logfile("app.log");
//   tee_buf tee(std::cout.rdbuf(), logfile.rdbuf());
//   std::ostream both(&tee);
//   both << "Written to stdout and app.log simultaneously\n";

Custom input buffer over a memory region

An input-only buffer requires setting up the get area with setg(). underflow() is only needed when the data spans multiple refill cycles; for a fixed memory region it is unnecessary:

cpp
#include <streambuf>
#include <istream>

class mem_buf : public std::streambuf {
public:
    mem_buf(const char* data, std::size_t len) {
        // setg takes non-const pointers β€” the interface predates const-correctness
        // in this area; we never write through gptr for an input-only buffer.
        char* p = const_cast<char*>(data);
        setg(p, p, p + len);
    }
    // No underflow() override needed: the full range is set up at construction.
};

// Usage:
//   const char raw[] = "42 3.14 hello";
//   mem_buf mb(raw, sizeof(raw) - 1);
//   std::istream is(&mb);
//   int n; double d; std::string s;
//   is >> n >> d >> s;  // n=42, d=3.14, s="hello"

Efficient file-to-string via istreambuf_iterator

istreambuf_iterator<CharT> reads directly from a basic_streambuf without formatting overhead, bypassing locale facets and numeric parsing. It is significantly faster than operator>> for raw text ingestion:

cpp
#include <fstream>
#include <iterator>
#include <string>

std::string slurp(const std::string& path) {
    std::ifstream f(path, std::ios::binary);
    return {std::istreambuf_iterator<char>(f),
            std::istreambuf_iterator<char>{}};
}

The iterator-range constructor of std::string performs this in a single pass with no intermediate copies.

Best Practices

Override xsputn() and xsgetn() for any performance-sensitive buffer. The default implementations call overflow()/underflow() one character at a time. Bulk overrides can use std::memcpy or equivalent, yielding orders-of-magnitude better throughput on large payloads.

Keep sync() honest. Returning 0 signals success; returning -1 sets badbit on the owning stream. If your buffer wraps a file descriptor or socket, flush it here. A sync() that always returns 0 while dropping data is worse than a hard failure.

Make streambuf subclasses non-copyable unless you've carefully reasoned through pointer ownership. The internal gptr/pptr machinery makes naive copy semantics fragile. Mark copy constructor and copy-assignment = delete by default.

Tie streams when synchronizing related buffers. ios::tie() causes one stream to flush before another is used. std::cin and std::cout are tied by default β€” this is why prompts appear before the cursor. Untying them with std::cin.tie(nullptr) can improve throughput at the cost of ordering guarantees.

Prefer std::osyncstream (C++20) over custom mutex-wrapping buffers for concurrent output. It queues a full atomic write per flush, avoiding interleaved output without manual locking.

Common Pitfalls

Confusing underflow() with uflow(). underflow() must not advance gptr() β€” it replenishes the buffer and returns the next character without consuming it. uflow() replenishes and consumes. Overriding the wrong one produces off-by-one reads that are difficult to diagnose.

Returning eof() from overflow() on success. overflow() must return traits_type::not_eof(c) β€” typically the written character itself β€” on success, and traits_type::eof() only on failure. Returning eof() unconditionally breaks the stream silently; writes appear to succeed at the call site but no data is stored.

sync_with_stdio(false) and mixing C stdio. By default, C++ streams synchronize with C's FILE* buffers. sync_with_stdio(false) decouples them for better performance, but after that call any mix of printf/scanf with std::cout/std::cin produces undefined ordering. Pick one interface and stay with it.

Dangling streambuf* after stream or buffer destruction. If you assign a streambuf* to a stream with rdbuf() and then destroy the buffer object, the stream holds a dangling pointer. The stream destructor will call pubsync() through it. Always restore the original buffer before any replacement buffer's lifetime ends.

setg and setp require non-const pointers. The C++98 interface was not designed with const data in mind. When wrapping read-only memory, const_cast is the accepted workaround, guarded by the invariant that the buffer never writes through gptr() in input-only mode.

Output Buffer with Internal Buffer Management

A production output buffer maintains its own contiguous storage and flushes to the real sink in bulk. This is the same architecture used by std::filebuf: setp marks the entire internal array as the put area, overflow() is called when the area is full, and sync() drains the pending range to the underlying device.

cpp
// C++11
#include <streambuf>
#include <array>
#include <unistd.h>   // write()

class LogBuf : public std::streambuf {
public:
    explicit LogBuf(int fd) : fd_(fd) {
        // Make the entire internal array the put area.
        setp(buf_.data(), buf_.data() + buf_.size());
    }

    ~LogBuf() override { sync(); }

protected:
    // Called when pptr() == epptr() (buffer full) or on explicit sputc() overflow.
    int_type overflow(int_type c) override {
        if (sync() == -1)
            return traits_type::eof();
        if (!traits_type::eq_int_type(c, traits_type::eof())) {
            // Write the overflowing character into the freshly reset buffer.
            *pptr() = traits_type::to_char_type(c);
            pbump(1);
        }
        return traits_type::not_eof(c);
    }

    // Flush [pbase(), pptr()) to the file descriptor and reset the put pointer.
    int sync() override {
        std::ptrdiff_t n = pptr() - pbase();
        if (n > 0) {
            if (::write(fd_, pbase(), static_cast<std::size_t>(n)) != n)
                return -1;
            setp(buf_.data(), buf_.data() + buf_.size());  // reset write pointer
        }
        return 0;
    }

private:
    int fd_;
    std::array<char, 4096> buf_;
};

// Usage:
//   LogBuf lb(STDOUT_FILENO);
//   std::ostream log_stream(&lb);
//   log_stream << "buffered write\n";  // only reaches the fd when buffer fills or flushes

sync() writes the buffered range [pbase(), pptr()) to the sink, then calls setp to reset pptr back to pbase. The next character written after a flush starts the cycle again. Returning -1 from sync() sets badbit on the owning stream automatically through pubsync().

rdbuf() RAII Guard for Safe Stream Redirection

The manual save-and-restore pattern (rdbuf(new_buf) followed later by rdbuf(saved)) is exception-unsafe: if anything throws between the two calls, the original buffer is never reinstated and the stream holds a dangling pointer. A small RAII guard eliminates this hazard entirely.

cpp
// C++11
#include <iostream>
#include <ostream>
#include <sstream>
#include <streambuf>

class StreamRedirect {
public:
    StreamRedirect(std::ostream& os, std::streambuf* new_buf)
        : os_(os), saved_(os.rdbuf(new_buf)) {}   // redirect in constructor

    ~StreamRedirect() { os_.rdbuf(saved_); }       // always restore

    StreamRedirect(const StreamRedirect&) = delete;
    StreamRedirect& operator=(const StreamRedirect&) = delete;

private:
    std::ostream&  os_;
    std::streambuf* saved_;
};

// Exception-safe capture of std::cout in a unit test:
void test_print_greeting() {
    std::ostringstream oss;
    {
        StreamRedirect rd(std::cout, oss.rdbuf());
        print_greeting();          // function under test
    }                              // rd destructor restores std::cout here
    assert(oss.str() == "Hello, world!\n");
}

Prefer StreamRedirect over manual save/restore in any code that may throw. This pattern is ubiquitous in C++ unit testing frameworks. C++20's std::osyncstream addresses concurrent output, but for single-threaded test capture the RAII redirect remains the standard approach.

Network/Socket-Backed streambuf

Any file descriptor or socket handle can be wrapped in a streambuf so that standard iostream operators work over the network. The input side overrides underflow() to call recv; the output side overrides overflow() and sync() to call send.

cpp
// C++11  (POSIX sockets)
#include <streambuf>
#include <sys/socket.h>

class SocketBuf : public std::streambuf {
public:
    explicit SocketBuf(int fd) : fd_(fd) {
        setg(get_buf_, get_buf_, get_buf_);             // empty get area initially
        setp(put_buf_, put_buf_ + sizeof(put_buf_));    // full put area
    }

protected:
    // Refill the get area from the socket.
    int_type underflow() override {
        ssize_t n = ::recv(fd_, get_buf_, sizeof(get_buf_), 0);
        if (n <= 0)
            return traits_type::eof();
        setg(get_buf_, get_buf_, get_buf_ + n);
        return traits_type::to_int_type(*gptr());
    }

    // Flush put area, then write the overflowing character.
    int_type overflow(int_type c) override {
        if (sync() == -1)
            return traits_type::eof();
        if (!traits_type::eq_int_type(c, traits_type::eof())) {
            *pptr() = traits_type::to_char_type(c);
            pbump(1);
        }
        return traits_type::not_eof(c);
    }

    // Drain the put buffer to the socket.
    int sync() override {
        std::ptrdiff_t n = pptr() - pbase();
        if (n > 0) {
            if (::send(fd_, pbase(), static_cast<std::size_t>(n), 0)
                    != static_cast<ssize_t>(n))
                return -1;
            setp(put_buf_, put_buf_ + sizeof(put_buf_));
        }
        return 0;
    }

private:
    int  fd_;
    char get_buf_[4096];
    char put_buf_[4096];
};

// Usage:
//   SocketBuf sock_buf(connected_fd);
//   std::iostream net_stream(&sock_buf);
//   net_stream << "GET / HTTP/1.0\r\n\r\n" << std::flush;
//   std::string line;
//   std::getline(net_stream, line);   // reads response line from socket

Disable Nagle's algorithm with TCP_NODELAY when writing many small protocol messages. Without it, the kernel may coalesce consecutive small send calls into a single TCP segment, introducing unexpected latency for request-response protocols. Setting TCP_NODELAY via setsockopt lets sync() deliver each flush immediately.

Hex-Encoding Filter Buffer

A filter buffer wraps another streambuf and transforms every character that passes through it. The underlying buffer handles the real I/O; the filter layer handles encoding. Filters compose: you can chain a tee buffer, a hex-encoding buffer, and a file buffer together without any of them knowing about the others.

cpp
// C++11
#include <streambuf>
#include <cstdio>   // std::snprintf

class HexEncodeBuf : public std::streambuf {
public:
    explicit HexEncodeBuf(std::streambuf* sink) : sink_(sink) {}

protected:
    // Each byte written is converted to two hex characters in the sink.
    int_type overflow(int_type c) override {
        if (traits_type::eq_int_type(c, traits_type::eof()))
            return traits_type::not_eof(c);
        char hex[3];
        std::snprintf(hex, sizeof(hex), "%02X",
                      static_cast<unsigned char>(traits_type::to_char_type(c)));
        if (sink_->sputn(hex, 2) != 2)
            return traits_type::eof();
        return c;
    }

    std::streamsize xsputn(const char_type* s, std::streamsize n) override {
        for (std::streamsize i = 0; i < n; ++i)
            if (traits_type::eq_int_type(overflow(traits_type::to_int_type(s[i])),
                                         traits_type::eof()))
                return i;
        return n;
    }

private:
    std::streambuf* sink_;
};

// Usage:
//   std::ofstream raw("data.hex");
//   HexEncodeBuf hex_buf(raw.rdbuf());
//   std::ostream hex_stream(&hex_buf);
//   hex_stream << "AB";   // writes "4142" into data.hex

Extension pattern: a checksum-appending buffer computes an XOR or CRC32 over every byte written via overflow() and emits the digest in sync(). Compose cleanly: tee_buf β†’ hex_buf β†’ file_buf is a valid chain β€” each layer sees only a std::streambuf* and calls sputc/sputn on it.

istreambuf_iterator vs operator>> Performance

std::istreambuf_iterator<char> reads raw characters directly from the underlying basic_streambuf, bypassing locale facets, whitespace skipping, and numeric parsing. It is the right tool whenever you need all bytes, including whitespace and binary data.

cpp
// C++98
#include <fstream>
#include <iterator>
#include <string>
#include <algorithm>

// Slurp an entire file into a string β€” 2-5Γ— faster than repeated operator>> calls.
std::string slurp_binary(const std::string& path) {
    std::ifstream f(path, std::ios::binary);
    return std::string(std::istreambuf_iterator<char>(f),
                       std::istreambuf_iterator<char>{});
}

// File copy using streambuf iterators β€” calls xsgetn/xsputn for bulk transfer.
void copy_file_streams(std::istream& in, std::ostream& out) {
    std::copy(std::istreambuf_iterator<char>(in),
              std::istreambuf_iterator<char>{},
              std::ostreambuf_iterator<char>(out));
}

operator>> on std::string skips leading whitespace, stops at the next whitespace boundary, and goes through locale facets β€” correct for tokenising formatted text, wrong for raw binary slurps. std::copy via istreambuf_iterator / ostreambuf_iterator is efficient because basic_streambuf::xsgetn and xsputn are called for bulk reads and writes internally.

Performance rule: use istreambuf_iterator for raw binary or full-text slurp; use operator>> for parsed numeric and string fields.

Stream Tying and Buffering Strategy

Three independent knobs control how C++ stream buffers interact with each other and with the C runtime: tie(), sync_with_stdio(), and disabling cin's tie. Misunderstanding any one of them causes subtle ordering bugs or leaves performance on the table.

cpp
// C++98 / C++11
#include <iostream>

// Default: std::cin is tied to std::cout.
// Before every read from cin, cout is flushed automatically.
// This guarantees that a prompt printed with cout appears before the cursor.

// Untying: removes the auto-flush.  Prompts and inputs may now interleave
// if the output buffer has not been explicitly flushed first.
std::cin.tie(nullptr);

// Decouple C++ streams from C stdio buffers.
// Safe only if printf/scanf are never mixed with cout/cin after this call.
std::ios_base::sync_with_stdio(false);

// Combined idiom for maximum throughput β€” standard in competitive programming:
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
// After these two calls, cin/cout use their own internal streambuf without
// any synchronisation overhead against FILE* or against each other.

sync_with_stdio(false) affects std::streambuf internals. With synchronisation enabled, the C++ stream buffers may defer to C's setvbuf machinery; after disabling it, each basic_streambuf subclass manages its own buffer independently, which is generally faster. Never call sync_with_stdio after any I/O has occurred β€” the standard permits this to have undefined behaviour.

cpp
// C++20 β€” atomic output without manual locking
#include <syncstream>

void worker(int id) {
    std::osyncstream(std::cout) << "Thread " << id << " reporting\n";
    // The osyncstream destructor flushes the entire line atomically.
}

std::osyncstream (C++20) wraps any std::ostream and queues writes atomically per flush, eliminating interleaved output in multithreaded code without a mutex. It is the recommended replacement for manual mutex-protected cout access.

See Also

  • std::filebuf β€” basic_streambuf implementation backed by a file descriptor
  • std::stringbuf β€” basic_streambuf implementation backed by a std::string
  • std::istreambuf_iterator / std::ostreambuf_iterator β€” raw iterator access to stream buffer contents
  • std::osyncstream β€” C++20 synchronized output stream wrapper for concurrent use
  • std::ios_base::sync_with_stdio β€” controls C / C++ I/O synchronization