Ch11.12: C++ iostream
Overview
C++’s standard I/O library provides std::fstream, std::ifstream,
and std::ofstream. These stream classes are built on top of
std::basic_filebuf, which is the C++ runtime’s buffered file abstraction.
fast_io provides wrappers that let you bring std::basic_filebuf
into the fast_io I/O system, so you can use print,
println, and scan on them.
Standard C++ I/O: <iostream>, <fstream>, <sstream>
Before discussing how fast_io wraps C++ I/O, let’s review the standard C++
I/O library. C++ provides three main headers for I/O:
<iostream>— standard streams (std::cin,std::cout,std::cerr,std::clog)<fstream>— file streams (std::ifstream,std::ofstream,std::fstream)<sstream>— string streams (std::istringstream,std::ostringstream,std::stringstream)
Standard Streams
C++ provides four predefined streams:
std::cin— standard input (usually keyboard)std::cout— standard output (usually terminal)std::cerr— standard error (unbuffered, usually terminal)std::clog— standard error (buffered, usually terminal)
#include <iostream>
#include <string>
int main() {
int x;
std::string name;
// Read from stdin
std::cout << "Enter your name: ";
std::cin >> name;
std::cout << "Enter a number: ";
std::cin >> x;
// Write to stdout
std::cout << "Hello, " << name << "!\n";
std::cout << "You entered: " << x << "\n";
// Write to stderr
if (x < 0) {
std::cerr << "Warning: negative number\n";
}
}
File Streams
#include <fstream>
#include <iostream>
#include <string>
int main() {
// Write to a file
std::ofstream fout("output.txt");
if (!fout) {
std::cerr << "Failed to open file\n";
return 1;
}
fout << "Hello, World!\n";
fout << "The answer is: " << 42 << "\n";
fout.close();
// Read from a file
std::ifstream fin("output.txt");
if (!fin) {
std::cerr << "Failed to open file\n";
return 1;
}
std::string line;
while (std::getline(fin, line)) {
std::cout << "Read: " << line << "\n";
}
fin.close();
// Read and write (bidirectional)
std::fstream fio("data.txt", std::ios::in | std::ios::out | std::ios::trunc);
fio << "Initial data\n";
fio.seekg(0); // Go back to beginning
std::getline(fio, line);
std::cout << "Read back: " << line << "\n";
}
Common file open modes:
std::ios::in— open for readingstd::ios::out— open for writingstd::ios::app— append to endstd::ios::ate— seek to end after openingstd::ios::trunc— truncate file if it existsstd::ios::binary— binary mode (no text translation)
String Streams
#include <sstream>
#include <iostream>
#include <string>
int main() {
// Write to a string
std::ostringstream oss;
oss << "x = " << 42 << ", pi = " << 3.14159;
std::string result = oss.str();
std::cout << "Built string: " << result << "\n";
// Read from a string
std::istringstream iss("42 3.14 hello");
int x;
double y;
std::string word;
iss >> x >> y >> word;
std::cout << "Parsed: " << x << ", " << y << ", " << word << "\n";
// Bidirectional string stream
std::stringstream ss;
ss << "100 200";
int a, b;
ss >> a >> b;
std::cout << "Sum: " << (a + b) << "\n";
}
Stream Manipulators
#include <iostream>
#include <iomanip>
int main() {
// Formatting
std::cout << std::hex << 255 << "\n"; // ff
std::cout << std::oct << 255 << "\n"; // 377
std::cout << std::dec << 255 << "\n"; // 255
// Floating-point precision
std::cout << std::fixed << std::setprecision(2) << 3.14159 << "\n"; // 3.14
std::cout << std::scientific << 3.14159 << "\n"; // 3.14e+00
// Width and alignment
std::cout << "[" << std::setw(10) << std::left << "left" << "]\n";
std::cout << "[" << std::setw(10) << std::right << "right" << "]\n";
// Boolean
std::cout << std::boolalpha << true << "\n"; // true
std::cout << std::noboolalpha << true << "\n"; // 1
}
Appendix: Complete C++ I/O Reference
Stream Classes
| Class | Description |
|---|---|
std::istream |
Base input stream |
std::ostream |
Base output stream |
std::iostream |
Base input/output stream |
std::ifstream |
Input file stream |
std::ofstream |
Output file stream |
std::fstream |
Input/output file stream |
std::istringstream |
Input string stream |
std::ostringstream |
Output string stream |
std::stringstream |
Input/output string stream |
std::filebuf |
File buffer (narrow) |
std::wfilebuf |
File buffer (wide) |
std::streambuf |
Base stream buffer |
Standard Stream Objects
| Object | Description |
|---|---|
std::cin |
Standard input stream |
std::cout |
Standard output stream |
std::cerr |
Standard error stream (unbuffered) |
std::clog |
Standard error stream (buffered) |
std::wcin |
Wide standard input stream |
std::wcout |
Wide standard output stream |
std::wcerr |
Wide standard error stream (unbuffered) |
std::wclog |
Wide standard error stream (buffered) |
Stream Manipulators
| Manipulator | Description |
|---|---|
std::endl |
Output newline and flush |
std::ends |
Output null terminator |
std::flush |
Flush stream |
std::ws |
Skip whitespace (input) |
std::boolalpha |
Use true/false for booleans |
std::noboolalpha |
Use 1/0 for booleans |
std::showbase |
Show base prefix (0x, 0) |
std::noshowbase |
Don’t show base prefix |
std::showpoint |
Show decimal point |
std::noshowpoint |
Don’t show decimal point |
std::showpos |
Show + sign for positive numbers |
std::noshowpos |
Don’t show + sign |
std::skipws |
Skip whitespace on input |
std::noskipws |
Don’t skip whitespace |
std::uppercase |
Use uppercase for hex/scientific |
std::nouppercase |
Use lowercase |
std::unitbuf |
Flush after each output |
std::nounitbuf |
Don’t flush after each output |
std::internal |
Internal padding |
std::left |
Left-align |
std::right |
Right-align |
std::dec |
Decimal base |
std::hex |
Hexadecimal base |
std::oct |
Octal base |
std::fixed |
Fixed-point notation |
std::scientific |
Scientific notation |
std::hexfloat |
Hexadecimal floating-point (C++11) |
std::defaultfloat |
Default floating-point notation (C++11) |
std::setprecision(n) |
Set decimal precision |
std::setw(n) |
Set field width |
std::setbase(n) |
Set numeric base |
std::setfill(c) |
Set fill character |
Stream State Flags
| Flag | Description |
|---|---|
std::ios::goodbit |
No error |
std::ios::eofbit |
End-of-file reached |
std::ios::failbit |
Logical error (e.g., failed to read int) |
std::ios::badbit |
Read/write error on stream |
Stream Member Functions
| Function | Description |
|---|---|
good() |
Check if stream is in good state |
eof() |
Check if end-of-file reached |
fail() |
Check if last operation failed |
bad() |
Check if stream is corrupted |
clear() |
Clear error state |
setstate() |
Set error state |
rdstate() |
Get current error state |
peek() |
Look at next character without extracting |
get() |
Extract one character |
getline() |
Extract a line |
put() |
Insert one character |
write() |
Insert multiple characters |
read() |
Extract multiple characters |
seekg() / seekp() |
Set get/put position |
tellg() / tellp() |
Get get/put position |
sync() |
Synchronize with underlying device |
flush() |
Flush output buffer |
fast_io Wrappers for std::basic_filebuf
There are two key types:
-
::fast_io::filebuf_file— an owning wrapper aroundstd::basic_filebuf*. It takes ownership of the underlying buffer and destroys it on destruction (RAII). -
::fast_io::filebuf_io_observer— a non-owning view over an existingstd::basic_filebuf*. It does not destroy the buffer on destruction.
Note: unlike the c_file family, filebuf_file only comes in two
character-type variants: ::fast_io::filebuf_file (narrow) and
::fast_io::wfilebuf_file (wide). The C++ standard library never implemented
basic_filebuf for other character types such as char8_t or
char32_t, so fast_io cannot offer them either.
Wrapping an Existing std::fstream
If you already have an std::fstream (or ifstream/ofstream)
and want to use fast_io operations on it, wrap its rdbuf() with
filebuf_io_observer. This is a non-owning view, so the fstream
remains responsible for the buffer’s lifetime.
#include <fast_io.h>
#include <fast_io_legacy.h>
#include <fstream>
int main()
{
using namespace ::fast_io::iomnp;
::std::ofstream fout("output.txt");
if (!fout)
{
return 1;
}
// Wrap the fstream's buffer in a non-owning observer.
::fast_io::filebuf_io_observer fiob{fout.rdbuf()};
// Now you can use fast_io's print/println on it.
println(fiob, "Hello from fast_io via fstream!");
::std::size_t const n{42zu};
println(fiob, "The answer is: ", n);
// fout still owns the buffer and will close the file on destruction.
}
Reading with scan
Just as with c_file and c_io_observer, you can use
scan on a filebuf_io_observer to read typed values.
#include <fast_io.h>
#include <fast_io_legacy.h>
#include <fstream>
int main()
{
using namespace ::fast_io::iomnp;
::std::ifstream fin("input.txt");
if (!fin)
{
return 1;
}
::fast_io::filebuf_io_observer fiob{fin.rdbuf()};
::std::size_t a{};
::std::size_t b{};
scan(fiob, a, b);
println("a + b = ", a + b);
}
Owning an fstream’s Buffer with filebuf_file
If you want to transfer ownership of the buffer out of an fstream and into
fast_io, you can move the filebuf into a filebuf_file.
After the move, the original fstream no longer owns the buffer.
#include <fast_io.h>
#include <fast_io_legacy.h>
#include <fstream>
int main()
{
::std::ofstream fout;
// Create a filebuf_file from scratch using open_mode.
::fast_io::filebuf_file fbf("owned.txt", ::fast_io::open_mode::out);
// Transfer the buffer into the ofstream.
*fout.rdbuf() = ::std::move(*fbf.fb);
// Now fout owns the buffer.
fout << "Written through std::ofstream after move\n";
// You can still use fast_io on the fbf observer, but the
// buffer has been moved, so be careful with the lifetimes.
}
Constructing an fstream from Syscalls (Preview)
fast_io can also go in the other direction: instead of wrapping an existing
fstream, you can open a file with fast_io’s low-level
syscall-based file types (such as posix_file or nt_file) and then
construct a std::fstream on top of it.
This gives you the best of both worlds — fast_io’s fast, direct
syscall path for opening and configuring the file, and then the ability to hand the file
off to existing code that expects an std::fstream.
#include <fast_io.h>
#include <fast_io_legacy.h>
#include <fstream>
int main()
{
// Open the file via fast_io's low-level syscall layer.
::fast_io::posix_file pf("syscall_file.txt", ::fast_io::open_mode::out);
// Wrap it in a filebuf_file (the C++ streambuffer layer).
::fast_io::filebuf_file fbf{::std::move(pf), ::fast_io::open_mode::out};
// Move the filebuf into an ofstream.
::std::ofstream fout;
*fout.rdbuf() = ::std::move(*fbf.fb);
fout << "Opened via syscall, written via fstream\n";
}
This technique — constructing an fstream from system calls — will
be covered in more detail in Ch11.17: Layers. For now, it is enough to know
that the path from syscalls to fstream exists and is well supported.
Sockets Are Just Files: The POSIX Philosophy
On POSIX systems (Linux, macOS, BSD, etc.), everything is a file. Sockets,
pipes, terminals, regular files — they all use the same file descriptor interface.
fast_io embraces this philosophy. On non-Windows systems,
::fast_io::native_file can wrap any file descriptor, including sockets.
Here’s an example showing how to construct a ::fast_io::filebuf_file from a
::fast_io::native_file wrapping a socket, then use C++ fstream to
read/write it:
#include <fast_io.h>
#include <fast_io_legacy.h>
#include <fstream>
int main()
{
using namespace ::fast_io::iomnp;
::fast_io::net_service net_svc;
// Connect to a server (returns a native_socket_file, which is native_file on POSIX)
auto dns = ::fast_io::native_dns_file(u8"example.com");
::fast_io::native_socket_file sock{::fast_io::tcp_connect(::fast_io::to_ip(dns, 80u16))};
// On POSIX, native_socket_file is just native_file (same type)
// We can construct a filebuf_file from it
::fast_io::filebuf_file fbf{::std::move(sock), ::fast_io::open_mode::in | ::fast_io::open_mode::out};
// Now we can use C++ fstream to read/write the socket!
::std::iostream sock_stream;
*sock_stream.rdbuf() = ::std::move(*fbf.fb);
// Send an HTTP request using fstream
sock_stream << "GET / HTTP/1.1\r\n"
<< "Host: example.com\r\n"
<< "Connection: close\r\n"
<< "\r\n";
sock_stream.flush();
// Read the response using fstream
::std::string line;
while (::std::getline(sock_stream, line))
{
::std::cout << line << '\n';
}
}
This example demonstrates a profound truth: sockets are just files. The C++
standard library’s fstream can work with sockets, pipes, terminals —
anything that has a file descriptor. The fact that C++ treats files and sockets as completely
different abstractions (with separate APIs like std::fstream vs Boost.Asio) is a
fundamental design flaw that violates the POSIX philosophy.
fast_io gets this right: there is no artificial separation between files and
sockets. A socket is a file. A pipe is a file. Everything uses the same print,
println, scan APIs. This is not just convenient — it’s
correct.
Why C++ iostream Is Fundamentally Flawed
C++ iostream has been around since the 1980s, but it suffers from deep design flaws that make
it unsuitable for modern C++. Understanding these flaws will help you appreciate why
fast_io was designed the way it is.
1. Security Vulnerabilities: Pointer Confusion
Consider this code:
#include <iostream>
#include <cstdint>
int main()
{
::std::int8_t* ptr{}; // nullptr
::std::cout << ptr << '\n'; // CRASH! Why?
}
This crashes because std::cout treats int8_t* (which is
signed char*) as a C-style string and tries to dereference it until it finds a
null terminator. Since the pointer is null, it crashes.
But change int8_t to int32_t:
#include <iostream>
#include <cstdint>
int main()
{
::std::int32_t* ptr{}; // nullptr
::std::cout << ptr << '\n'; // No crash, prints 0
}
Now it prints 0 (the pointer value) because int32_t* is not treated
as a string. The behavior changes based on the exact type of the pointer, which is
confusing and error-prone.
fast_io avoids this entirely: pointers are always printed as addresses, never
dereferenced as strings. You must explicitly use ::fast_io::mnp::os_c_str() to
print a C-string.
2. Not Thread-Safe
std::cout is shared across all threads. If multiple threads write to it
simultaneously, their output can interleave:
#include <iostream>
#include <thread>
void worker(int id)
{
// BAD: Output can interleave across threads
::std::cout << "Thread " << id << " is running\n";
}
int main()
{
::std::jthread t1(worker, 1);
::std::jthread t2(worker, 2);
// Possible output: "Thread Thread 12 is running\n is running\n"
}
With fast_io, each print or println call is atomic
(for console output). The output will never interleave:
#include <fast_io.h>
#include <thread>
void worker(int id)
{
using namespace ::fast_io::iomnp;
// GOOD: Each println is atomic
println("Thread ", id, " is running");
}
int main()
{
::std::jthread t1(worker, 1);
::std::jthread t2(worker, 2);
// Output is always clean: "Thread 1 is running\nThread 2 is running\n"
// (order may vary, but no interleaving)
}
3. Not Exception-Safe
If an exception is thrown while formatting output, the stream’s internal state can be left in an inconsistent state:
#include <iostream>
#include <stdexcept>
struct BadType {
friend ::std::ostream& operator<<(::std::ostream& os, BadType const&) {
throw ::std::runtime_error("oops");
}
};
int main()
{
try {
::std::cout << "Value: " << BadType{} << "\n";
} catch (...) {
// Stream may be in a bad state now
}
// ::std::cout might be unusable here
}
fast_io’s formatting is designed to be exception-safe and doesn’t
leave streams in inconsistent states.
4. Stateful Manipulators: The <iomanip> Nightmare
C++ manipulators like std::hex, std::dec, std::setw
change the stream’s state. This leads to subtle bugs:
#include <iostream>
#include <iomanip>
void print_data(int value)
{
::std::cout << ::std::hex << value << '\n';
// Stream is now in hex mode!
}
int main()
{
print_data(255); // Prints "ff"
::std::cout << 255 << '\n'; // Also prints "ff"! (not "255")
// You must manually restore the state:
::std::cout << ::std::dec; // Restore to decimal
}
This is a maintenance nightmare. You must remember to restore the stream state after every use, or else later code will behave unexpectedly.
fast_io’s manipulators are stateless:
#include <fast_io.h>
void print_data(int value)
{
using namespace ::fast_io::iomnp;
println(::fast_io::mnp::hex(value)); // Only this value is hex
// No state to restore!
}
int main()
{
using namespace ::fast_io::iomnp;
print_data(255); // Prints "ff"
println(255); // Prints "255" (decimal, as expected)
}
Each manipulator applies only to the value it wraps. There is no global state to corrupt.
5. Locale Hell
C++ iostream respects the global locale, which can cause unexpected behavior:
#include <iostream>
#include <locale>
int main()
{
::std::locale::global(::std::locale("de_DE")); // German locale
::std::cout << 1234.56 << '\n'; // Prints "1.234,56" (German format)
// Now your output format depends on the user’s locale!
// This breaks parsers, config files, network protocols, etc.
}
fast_io ignores locales by default. Numbers are always formatted in a
machine-readable, locale-independent way. If you need locale-aware formatting, you must
explicitly request it.
6. Performance
C++ iostream is slower than C’s printf family due to virtual function calls,
locale handling, and complex formatting logic. fast_io is faster than both
printf and iostream, often by a large margin.
Practical Advice: Implementing fstream with fast_io
If you are working with legacy code that uses C++ fstream, you can internally use
::fast_io::filebuf_io_observer to implement it. This gives you the performance
benefits of fast_io’s low-level operations while maintaining compatibility
with the fstream interface.
The cost is much smaller than you might think. filebuf_io_observer is just a
lightweight wrapper around std::basic_filebuf*, and the underlying operations
(read, write, seek) can be implemented using fast_io’s efficient syscall
layer.
This is a common pattern in high-performance libraries: expose a standard interface (like
fstream) for compatibility, but use fast_io internally for speed.
Appendix: Complete <iomanip> Reference
The <iomanip> header provides manipulators that take arguments. These
manipulators modify the stream’s internal state, which persists until explicitly changed.
This is a fundamental design flaw — see “Why C++ iostream Is Fundamentally Flawed”
above. fast_io’s manipulators are stateless and do not suffer from these issues.
Field Width and Fill
| Manipulator | Description | Example |
|---|---|---|
std::setw(n) |
Set minimum field width to n characters |
std::cout << std::setw(10) << 42; |
std::setfill(c) |
Set fill character to c |
std::cout << std::setfill('0') << std::setw(5) << 42; |
Warning: std::setw only applies to the next output
operation, then resets to 0. This is inconsistent and error-prone.
Numeric Base
| Manipulator | Description | Example |
|---|---|---|
std::setbase(n) |
Set numeric base to n (8, 10, or 16) |
std::cout << std::setbase(16) << 255; (prints ff) |
Precision
| Manipulator | Description | Example |
|---|---|---|
std::setprecision(n) |
Set floating-point precision to n digits |
std::cout << std::setprecision(3) << 3.14159; (prints 3.14) |
Note: Precision persists until changed. Combined with
std::fixed or std::scientific, it controls decimal places.
Other Manipulators
| Manipulator | Description |
|---|---|
std::resetiosflags(f) |
Clear specified format flags |
std::setiosflags(f) |
Set specified format flags |
Format flags include: std::ios::left, std::ios::right,
std::ios::internal, std::ios::dec, std::ios::hex,
std::ios::oct, std::ios::fixed, std::ios::scientific,
std::ios::boolalpha, std::ios::showbase,
std::ios::showpoint, std::ios::showpos,
std::ios::skipws, std::ios::uppercase,
std::ios::unitbuf.
Why This Is Terrible
All of these manipulators modify the stream’s internal state. Consider:
#include <iostream>
#include <iomanip>
void print_hex(int value) {
std::cout << std::hex << value << '\n';
// Stream is now in hex mode!
}
int main() {
print_hex(255); // Prints "ff"
std::cout << 255 << '\n'; // Also prints "ff"!
// Must manually restore: std::cout << std::dec;
}
This is a maintenance nightmare. You must remember to restore the stream state after every use, or else later code will behave unexpectedly. Exceptions can leave the stream in an inconsistent state. Multiple threads can corrupt each other’s formatting.
fast_io’s manipulators are stateless:
#include <fast_io.h>
void print_hex(int value) {
using namespace ::fast_io::iomnp;
println(::fast_io::mnp::hex(value));
// No state to restore!
}
int main() {
using namespace ::fast_io::iomnp;
print_hex(255); // Prints "ff"
println(255); // Prints "255" (decimal, as expected)
}
Each manipulator applies only to the value it wraps. There is no global state to corrupt.
Key Takeaways
-
::fast_io::filebuf_fileowns astd::basic_filebuf*with RAII semantics. -
::fast_io::filebuf_io_observeris a non-owning view over an existingstd::basic_filebuf*, perfect for wrapping anfstream’srdbuf(). -
Only narrow (
filebuf_file) and wide (wfilebuf_file) variants exist, because the C++ standard library does not providebasic_filebuffor other character types. -
Sockets are just files. On POSIX systems, you can wrap a socket in a
filebuf_fileand usefstreamto read/write it. C++’s artificial separation of files and sockets (e.g., Boost.Asio vs fstream) violates the POSIX philosophy. -
C++ iostream has fundamental design flaws: pointer confusion (security
issues), not thread-safe, not exception-safe, stateful manipulators that corrupt stream
state, locale-dependent output, and poor performance.
fast_iofixes all of these. -
fast_io’s manipulators are stateless. Unlike
std::hexwhich changes the stream state,::fast_io::mnp::hex(value)applies only to that value. No state to restore, no bugs. -
Use fast_io internally for fstream implementations. If you need to
maintain compatibility with
fstream, use::fast_io::filebuf_io_observerinternally. The performance cost is minimal, and you get the benefits offast_io’s efficient syscall layer.