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:

Standard Streams

C++ provides four predefined streams:


#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:

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:

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