We’ve seen a bit of I/O in the C Standard Library already, but this isn’t C++’s main way to perform I/O. Today we’ll look at the “streams” API that’s designed around C++’s support for strong types and overloaded operators rather than facilities like “format strings.” We’ll also see how to write the canonical “Hello, world!” program in C++ and how to finally implement DebugLog!

Table of Contents

Library Layout and iosfwd

The I/O library subset of the broader C++ Standard Library contains several header files that often #include each other. Here’s how those relationships look:

I/O Library

The most basic usage of the I/O library is to #include <iosfwd>. This header provides “forward” declarations of I/O types. These can then be named, such as by pointer or reference types. They can’t be used by value or by accessing any of their members. <iosfwd> really just exists to speed up compilation when I/O types only need to be named and their full definitions, which are slower to compile, aren’t needed.

We’ll see all the types in <iosfwd> as we go through each of the headers that #include it and then provide definitions.

ios

The <ios> header contains basic tools that the rest of the I/O library uses. First up, there’s std::ios_base which serves as the abstract base class of all the I/O “stream” classes. We’ll see what those classes look like soon, but suffice to say a “stream” is an abstract input and/or output that can be backed by a file, “standard output”, a string, and so forth. It’s very similar to the C# Stream abstract base class.

Here’s some of what std::ios_base provides:

#include <ios>
#include <locale>
 
void Goo(std::ios_base& base)
{
    // Get the flags that control formatting
    std::ios_base::fmtflags f{ base.flags() };
    DebugLog((f & std::ios_base::dec) != 0); // Maybe true
    DebugLog((f & std::ios_base::hex) != 0); // Maybe false
    DebugLog((f & std::ios_base::boolalpha) != 0); // Maybe false
 
    // Set and unset a format flag
    base.setf(std::ios_base::boolalpha);
    DebugLog((base.flags() & std::ios_base::boolalpha) != 0); // true
    base.unsetf(std::ios_base::boolalpha);
    DebugLog((base.flags() & std::ios_base::boolalpha) != 0); // false
 
    // Set and get floating-point precision
    base.precision(2);
    DebugLog(base.precision()); // 2
 
    // Set and get the minimum number of characters that some operations print
    base.width(10);
    DebugLog(base.width()); // 10
 
    // Set and get the locale
    base.imbue(std::locale{ "de-DE" });
    DebugLog(base.getloc().name()); // de-DE
 
    try
    {
        throw std::ios_base::failure{ "some I/O error" };
    }
    catch (const std::ios_base::failure& ex)
    {
        DebugLog(ex.what()); // some I/O error
    }
 
    // Ways of opening streams
    // These are bit flags to form a mask
    std::ios_base::openmode mode{
        std::ios_base::app | // Append
        std::ios_base::binary | // Binary
        std::ios_base::in | // Read
        std::ios_base::out | // Write
        std::ios_base::trunc | // Overwrite
        std::ios_base::ate // Open at end of stream
    };
 
    // Bit flags forming the state of a stream
    std::ios_base::iostate state{
        std::ios_base::goodbit | // No error
        std::ios_base::badbit | // Unrecoverable error
        std::ios_base::failbit | // Operation failed (e.g. formatting failed)
        std::ios_base::eofbit // End of stream
    };
 
    // Directions to seek
    std::ios_base::seekdir dir = std::ios_base::beg; // Beginning of stream
    dir = std::ios_base::end; // End of stream
    dir = std::ios_base::cur; // From the current position
}

There’s also std::char_traits, which is a class template with static functions that provide functionality for operations on particular kinds of characters:

#include <ios>
 
// Single-character operations
DebugLog(std::char_traits<char>::eq('a', 'a')); // true
DebugLog(std::char_traits<char>::eof()); // -1
 
// Copy multiple characters
char buf[5];
std::char_traits<char>::copy(buf, "abcd", 4);
DebugLog(buf); // abcd
 
// Lexicographical comparison
DebugLog(std::char_traits<char>::compare("abcd", "efgh", 4)); // -1

std::fpos is then build on std::char_traits to represent a position in a file. Usually we use the provided type aliases:

Type Alias Character Traits
std::streampos std::char_traits<char>
std::wstreampos std::char_traits<wchar_t>
std::u8streampos std::char_traits<char8_t>
std::u16streampos std::char_traits<char16_t>
std::u32streampos std::char_traits<char32_t>

Finally, there are some free functions that set flags on std::ios_base objects as an alternative to the setf member function:

#include <ios>
 
void Goo(std::ios_base& base)
{
    // Use strings like "true" or numbers like 1 for bools
    std::boolalpha(base);
    std::noboolalpha(base);
 
    // Use uppercase or lowercase in hexadecimal numbers and floats
    std::uppercase(base);
    std::nouppercase(base);
}
streambuf

The <streambuf> header provides just one class: std::basic_streambuf. This is an abstract base class of a way to input and output characters. It’s meant to have its virtual functions overridden by derived classes in such a way that they implement the actual reading and writing from the stream. This might mean access to a network socket, file system, GPU memory, or any other place that serialized data can be transmitted to and received from.

We don’t usually have a need to derive from this class. Instead, we typically use derivations that are already provided by the C++ Standard Library. These include “standard output, “standard error,” “standard input,” and access the file system. We’ll see these when we look at <iostream> and <fstream>.

ostream and iostream

The <ostream> header provides output streams via the std::basic_ostream class template. There are two aliases for this that we typically use: std::ostream which aliases std::basic_ostream<char> and std::wostream for wchar_t.

we need to pass a std::basic_streambuf to construct a std::basic_ostream. This is also not commonly done. Instead, we typically use an already-created std::basic_ostream object. The <iostream> header provides a few pairs of these:

Global Object Use C# Equivalent
std::cout Standard output of char Console.OpenStandardOutput
std::wcout Standard output of wchar_t Console.OpenStandardOutput
std::clog Standard error of char Console.OpenStandardError
std::wclog Standard error of wchar_t Console.OpenStandardError
std::cerr Unbuffered standard error of char Console.OpenStandardError
std::wcerr Unbuffered standard error of wchar_t Console.OpenStandardError

Regardless of the object we choose to use, we have a few options for outputting data. The most typical is “formatted output” via the overloaded << operator. This leads to the canonical “Hello, world!” application for C++:

#include <iostream>
 
int main()
{
    std::cout << "Hello, world!\n";
}

The << operator is overloaded with all of the primitive types like long, float, char, char* (a C-string), and bool. It’s common for us to add an overload for our own types so we can format them for output:

#include <iostream>
 
// Our own type
struct Point2
{
    float X;
    float Y;
};
 
// Overload basic_ostream's << operator for our own type
template <typename TChar>
std::basic_ostream<TChar>& operator<<(
    std::basic_ostream<TChar>& stream,
    const Point2& point)
{
    // Use the overloaded << operator with already-supported primitive types
    stream << '(' << point.X << ", " << point.Y << ')';
 
    // Return the stream for operator chaining
    return stream;
}
 
// Print our own type to standard output
Point2 p{ 2, 4 };
std::cout << p << '\n'; // (2, 4)\n

At long last, we can write the DebugLog function! With the support of variadic templates, template specialization, and type-aware formatted output to a std::basic_ostream, it’s actually only about 9 lines of code:

#include <iostream>
 
// Logging nothing just prints an empty line
void DebugLog()
{
    std::cout << '\n';
}
 
// Logging one value. This is the base case.
template <typename T>
void DebugLog(const T& val)
{
    std::cout << val << '\n';
}
 
// Logging two or more values
template <typename TFirst, typename TSecond, typename ...TRemain>
void DebugLog(const TFirst& first, const TSecond& second, TRemain... remain)
{
    // Log the first value
    std::cout << first << ", ";
 
    // Recurse with the second value and any remaining values
    DebugLog(second, remain...);
}
 
// Call the first function to print an empty line
DebugLog(); // \n
 
// Call the second function to print a single value
DebugLog('a'); // a\n
 
// Call the third function
// It prints "b, "
// It recurses with (1, true, "hello")
// It prints "1, "
// It recurses with (true, "hello")
// It prints "true, "
// It calls the second function with "hello"
// The second function prints "hello\n"
DebugLog('b', 1, true, "hello"); // b, 1, true, hello\n

Besides formatted output, there’s also unformatted output for when we want to write raw data to an output stream. This data can be either a single character or a block of characters. We typically use this for outputting binary data while formatted output is typically used for strings and other human-readable data:

#include <iostream>
 
// Unformatted output of a single character
std::cout.put('a');
 
// Unformatted output of a block of characters
char buf[8];
for (int i = 0; i < sizeof(buf); ++i)
{
    buf[i] = 'a' + i;
}
std::cout.write(buf, sizeof(buf)); // abcdefgh

There are also functions for querying and controlling the position in the output stream. This has no meaning for std::cout, but makes sense for other output streams such as to files:

#include <iostream>
 
// Write a null byte at a position then restore the position
void WriteNullAt(std::ostream& stream, std::streampos pos)
{
    // Get stream position
    std::streampos oldPos{ stream.tellp() };
 
    // Seek stream position
    stream.seekp(pos);
 
    // Write the null byte
    stream.put(0);
 
    // Seek the stream back
    stream.seekp(oldPos);
}

An output stream that’s buffered can also be explicitly flushed using, well, flush:

std::cout.flush();

<ostream> also provides a few “manipulator” functions. These are functions that, when passed to << for formatted output, are called with the stream to determine what to output. We typically use them like this:

#include <iostream>
 
// endl prints a "\n" character then calls flush()
std::cout << "Hello, world!" << std::endl;
 
// ends prints a null character, i.e. the value 0
std::cout << "Hello, world!" << std::ends;
istream

The <istream> header provides the opposite of <ostream>: support for input streams. The std::basic_istream class and its std::istream and std::wistream aliases make this possible. There’s also a std::basic_iostream for streams that can input and output along with std::iostream and std::wiostream aliases. The <iostream> header provides std::cin and std::wcin global objects to read from “standard input.”

As with output, we have both “formatted” and “unformatted” reading options. The “formatted” option enables the classic command line application to implement a basic calculator using the overloaded >> operator:

#include <iostream>
 
int main()
{
    std::cout << "Enter x:" << std::endl;
    int x;
    std::cin >> x;
 
    std::cout << "Enter y:" << std::endl;
    int y;
    std::cin >> y;
 
    std::cout << "x + y is " << (x+y) << std::endl;
}

Entering in some test values when prompted, we get the following output:

Enter x:
2
Enter y:
4
x + y is 6

We also have several options for unformatted input:

#include <iostream>
 
// Read 3 characters then print them
char buf[4] = { 0 };
std::cin.read(buf, 3); // Enter "abc"
DebugLog(buf); // abc
 
//// Read 1 character and ignore it
std::cin.ignore(1);
 
// Read until a character is found or the end of the buffer is hit
std::cin.getline(buf, sizeof(buf), ';'); // Enter "ab;c"
DebugLog(buf); // ab
std::cin.getline(buf, sizeof(buf), ';'); // Enter "abcdefg"
DebugLog(buf); // abc
 
// Put a character into the input stream
std::cin.putback('a');
std::cin.read(buf, 1);
DebugLog(buf); // a
iomanip

The <iomanip> header is full of “manipulator” functions that we can pass to formatted read and write operations. Here’s a sampling of the options:

#include <iomanip>
#include <iostream>
#include <numbers>
 
using namespace std;
 
// Output 255 as hexadecimal
cout << setbase(16) << 255 << endl; // ff
 
// Output pi with 3 digits of precision (whole and fractional)
cout << setprecision(3) << numbers::pi << endl; // 3.14
 
// Set the width of the output and how it's filled. Useful for columns.
auto row = [](auto num, auto name, char fill = ' ') {
    cout << '|' << setw(10) << setfill(fill) << name << '|';
    cout << setw(10) << setfill(fill) << num << '|' << endl;
};
row("Number", "Name");
row('-', '-', '-');
row(1, "One");
row(2, "Two");
// Prints:
// |    Number|      Name|
// |----------|----------|
// |         1|       One|
// |         2|       Two|
 
// Output cents as US Dollars
cout.imbue(locale("en_US"));
cout << std::showbase << put_money(250) << endl; // $2.50
fstream

The <fstream> header has facilities for file system I/O. At the lowest level, we have std::basic_filebuf which is a std::basic_streambuf that we can use for raw file system access. More typically, we use the std::basic_ifstream, std::basic_ofstream, and std::basic_fstream classes for input, output, and both. Aliases such as std::fstream are provided and most commonly seen. These are the rough equivalent of FileStream in C#:

#include <fstream>
 
void Foo()
{
    // Open the file for writing
    std::fstream stream{ "/path/to/file", std::ios_base::out };
 
    // Formatted write to the file, including a flush via endl
    stream << "hello" << std::endl;
} // fstream's destructor closes the file

As a derivative of basic_iostream, basic_fstream inherits all of its functionality. This includes the formatted and unformatted I/O functions such as the overloaded << operator seen above. Besides this, a few file-specific member functions are on offer:

#include <fstream>
 
void Foo()
{
    // Open the file for writing
    std::fstream stream{ "/path/to/file", std::ios_base::out };
 
    // Check if the file is open
    DebugLog(stream.is_open()); // true
 
    // Explicitly close the file without waiting for the destructor
    stream.close();
    DebugLog(stream.is_open()); // false
 
    // Explicitly open a file without creating a new stream
    stream.open("/path/to/other/file", std::ios_base::out);
} // fstream's destructor closes any open file
sstream

As C# has StringBuilder, C++ has std::basic_ostringstream in the <sstream> header. This class template, typically aliased as std::ostringstream, allows writing to a string via the stream API:

#include <sstream>
 
// Create a stream for an empty string
std::ostringstream stream{};
 
// Formatted writing
stream << "Hello" << 123;
 
// Unformatted writing
stream.write("Goodbye", 8);
 
// Get a string for what was written
std::string str{ stream.str() };
DebugLog(str); // Hello123Goodbye

There’s also an input version that reads from strings:

#include <sstream>
 
// Create a stream for a string
std::istringstream stream{ "Hello 123Goodbye" };
 
// Formatted reading
std::string str;
int num;
stream >> str >> num;
DebugLog(str); // Hello
DebugLog(num); // 123
 
// Unformatted reading
char buf[8] = { 0 };
stream.read(buf, 8);
DebugLog(buf); // Goodbye

And there’s a combined std::stringstream that can both read and write:

#include <sstream>
 
// Create a stream for an empty string
std::stringstream stream{};
 
// Formatted writing
stream << "Hello 123";
 
// Change read position to the beginning
stream.seekg(std::ios_base::beg, 0);
 
// Formatted reading
std::string str;
int num;
stream >> str >> num;
DebugLog(str); // Hello
DebugLog(num); // 123
syncstream

The final header of the I/O library was introduced with C++20: <syncstream>. It provides std:: basic_syncbuf and std::basic_osyncstream to synchronize the writing to a stream from multiple threads. One motivating example is printing logs to standard output. Consider how this works without synchronization:

#include <iostream>
#include <thread>
#include <chrono>
#include <functional>
 
// Prints "helloworld" to standard output 100 times
void Print(std::ostream& stream)
{
    for (int i = 0; i < 100; ++i)
    {
        stream << "helloworld" << std::endl;
        std::this_thread::sleep_for(std::chrono::microseconds{ 1 });
    }
}
 
// Spawn a thread to print
std::jthread t{ Print, std::ref(std::cout) };
 
// Print on the main thread while the thread is running
Print(std::cout);

The exact output depends on OS scheduling, but this is likely to produce errors due to contention for the output stream and its internal buffer:

helloworld
helloworld
helloworldhelloworld
 
helloworld
helloworld
helloworld
helloworld

Here one of the threads printed helloworld but the other thread interrupted to print helloworld\n before the first thread could print its \n character. When the first thread resumed execution, it printed that \n resulting in two \n in a row: \n\n.

To avoid this problem, or any contention due to multi-threaded writing to a shared stream, we can use std::basic_osyncstream or its std::osyncstream alias to synchronize the writes:

#include <syncstream>
#include <iostream>
#include <thread>
#include <chrono>
#include <functional>
 
void Print(std::ostream& stream)
{
    for (int i = 0; i < 100; ++i)
    {
        stream << "helloworld" << std::endl;
        std::this_thread::sleep_for(std::chrono::microseconds{ 1 });
    }
}
 
// Create a synchronized stream backed by std::cout
std::osyncstream out{ std::cout };
 
// Print to the synchronized stream
std::jthread t{ Print, std::ref(out) };
Print(std::cout);
Conclusion

The C++ “I/O streams” library is far more powerful than basic functionality like printf found in the C Standard Library. It’s not nearly as error-prone since it makes use of the C++ type system rather than manually-entered “format strings.” It’s far more extensible since we can write our own format functions, manipulator functions, and stream types to read and write from whatever kind of device we encounter.

Compared to C#, its “unformatted” options are similar to byte-based options such as we find in the base Stream class. Its formatted options are similar to what we find in classes like TextReader and TextWriter except adapters like these aren’t required in C++. On the whole, the two libraries provide comparable functionality and even share the “stream” abstraction and terminology.

Perhaps the largest difference is in extensibility where C++ allows us to write our own types directly to a stream while C# typically requires us to allocate a String object from our ToString function. The addition of std::osyncstream in C++20 is also a nice addition as it saves us from multi-threaded synchronization of stream writes regardless of language.