C++ For C# Developers: Part 50 – I/O Library
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Destructuring and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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:
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.