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++98The 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:
using std::streambuf = std::basic_streambuf<char>; // C++98
using std::wstreambuf = std::basic_streambuf<wchar_t>; // C++98The concrete subclasses bundled with the standard library are:
std::filebuf/std::wfilebufβ backsfstreamobjects; defined in<fstream>std::stringbuf/std::wstringbufβ backsstringstreamobjects; 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:
| Area | Begin | Current | End |
|---|---|---|---|
| Get | eback() | gptr() | egptr() |
| Put | pbase() | 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
#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:
#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):
#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:
#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:
#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.
// 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 flushessync() 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.
// 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.
// 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 socketDisable 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.
// 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.hexExtension 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.
// 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.
// 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.
// 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_streambufimplementation backed by a file descriptorstd::stringbufβbasic_streambufimplementation backed by astd::stringstd::istreambuf_iterator/std::ostreambuf_iteratorβ raw iterator access to stream buffer contentsstd::osyncstreamβ C++20 synchronized output stream wrapper for concurrent usestd::ios_base::sync_with_stdioβ controls C / C++ I/O synchronization