Demystifying C++ I/O
A program’s fundamental job is to accept input and produce output. A program that produces no output of any sort would not be very useful. All languages provide some mechanism for I/O, either as a built-in part of the language or through an OS-specific API. A good I/O system is both flexible and easy to use. Flexible I/O systems support input and output through a variety of devices, such as files and the user console. Files could be standard files but could also be data coming from a variety of sources such as Internet of Things (IoT) devices, web services, and more. It could be weather data from a weather device or stock values from a stockbroker web service. Flexible I/O systems also support reading and writing of different types of data. I/O is error-prone because data coming from a user can be incorrect or the underlying filesystem or other data source can be inaccessible. Thus, a good I/O system is also capable of handling error conditions.
If you are familiar with the C language, you have undoubtedly used printf() and scanf(). As I/O mechanisms, printf() and scanf()are certainly flexible. Through escape codes and variable placeholders (similar to format specifiers and replacement fields for std::format(), print(), and println() as discussed in Chapter 2, “Working with Strings and String Views”), they can be customized to read in specially formatted data or output any value that the formatting codes permit. Supported types are limited to integer/character values, floating-point values, and strings. However, printf() and scanf() falter on other measures of good I/O systems. They do not handle errors particularly well. For example, if you tell them to interpret a floating-point number as an integer, they will happily do so. Additionally, they are not flexible enough to handle custom data types, they are not type safe, and in an object-oriented language like C++, they are not at all object oriented.
C++ provides a more refined, more flexible, and object-oriented approach to I/O. Streams are encapsulated in classes that result in a user-friendly and safe solution. In this chapter, you will first learn what streams are and then learn how to use streams for data output and input. You will also learn how to use the stream mechanism to read from various sources and write to various destinations, such as the user console, files, and even strings. This chapter covers the most commonly used I/O features.
Almost all examples in this book use print() and println() to print text to the user console. An alternative is to use the I/O streaming functionality discussed in this chapter. I recommend using print() and println() instead of streaming to standard output, as the former is easier to read, more compact, and more performant. However, this chapter discusses I/O streaming in detail, as it’s still important to know how it works in C++ because you’ll undoubtedly have to work with code that uses I/O streaming.
The last part of this chapter discusses the filesystem support library provided by the C++ Standard Library. This library allows you to work with paths, directories, and files, and it nicely complements the mechanisms provided for I/O by the streams.
USING STREAMS
Section titled “USING STREAMS”The stream metaphor takes a bit of getting used to. At first, streams may seem more complex than traditional C-style I/O, such as printf(). In reality, they seem complicated initially only because there is a deeper metaphor behind streams than there is behind printf(). Don’t worry, though: after a few examples, everything will be clear.
What Is a Stream, Anyway?
Section titled “What Is a Stream, Anyway?”Chapter 1, “A Crash Course in C++ and the Standard Library,” compares the cout stream to a laundry chute for data. You throw some variables down the stream, and they are written to the user’s screen, or console. More generally, all streams can be viewed as data chutes. Streams vary in their direction and their associated source or destination. For example, the cout stream that you are already familiar with is an output stream, so its direction is “out.” It writes data to the console, so its associated destination is “console.” The c in cout does not stand for “console” as you might expect but stands for “character” as it’s a character-based stream. There is another standard stream called cin that accepts input from the user. Its direction is “in,” and its associated source is “console.” As with cout, the c in cin stands for “character.” Both cout and cin are predefined instances of streams available in the std namespace. The following table gives a brief description of all predefined streams defined in <iostream>.
Streams can be buffered or unbuffered. The difference between them is that a buffered stream does not immediately send the data to the destination. Instead, it buffers, that is collects, incoming data and then sends it in blocks. An unbuffered stream, on the other hand, immediately sends the data to the destination. Buffering is usually done to improve performance, as certain destinations, such as files, perform better when writing bigger blocks at once. Note that you can always force a buffered stream to send all its currently buffered data to the destination by flushing its buffer using the flush() member function. Buffering and flushing is discussed in a bit more detail later in this chapter.
| STREAM | DESCRIPTION |
|---|---|
| cin | An input stream, reads data from the “input console” |
| cout | A buffered output stream, writes data to the “output console” |
| cerr | An unbuffered output stream, writes data to the “error console,” which is often the same as the “output console” |
| clog | A buffered version of cerr |
Remember from Chapter 1 that std::print() and println() by default print to cout but that you can pass a stream as first argument to these functions if you want to print to a different stream, for example:
println(cerr, "This is an error printed to cerr.");There are also wide-character, wchar_t versions available of these streams that have names starting with w: wcin, wcout, wcerr, and wclog. Wide characters can be used to work with languages that have more characters than, for example, English, such as Chinese. Wide characters are discussed in Chapter 21, “String Localization and Regular Expressions.”
Another important aspect of streams is that they include data but also have a current position. The current position is the position in the stream where the next read or write operation will take place.
Stream Sources and Destinations
Section titled “Stream Sources and Destinations”Streams as a concept can be applied to any object that accepts data or emits data. You could write a stream-based network class or stream-based access to a Musical Instrument Digital Interface (MIDI) instrument. In C++, there are four common sources and destinations for streams: consoles, files, strings, and fixed buffer arrays. Fixed buffer array support is introduced with C++23.
You have already seen many examples of user, or console, streams. Console input streams make programs interactive by allowing input from the user at run time. Console output streams provide feedback to the user and output results.
File streams, as the name implies, read data from and write data to a filesystem. File input streams are useful for reading configuration data and saved files or for batch processing file-based data. File output streams are useful for saving state and providing output. If you are familiar with C-style input and output, then file streams subsume the functionality of the C functions fprintf(), fwrite(), and fputs() for output, and fscanf(), fread(), and fgets() for input. As these C-style functions are not recommended in C++, they are not further discussed.
String streams are an application of the stream metaphor to the string type. With a string stream, you can treat character data just as you would treat any other stream. For the most part, this is merely a handy syntax for functionality that could be handled through member functions on the string class. However, using stream syntax provides opportunities for optimization and can be far more convenient and more efficient than direct use of the string class. String streams subsume the functionality of sprintf(), sprintf_s(), sscanf(), and other forms of C-style string-formatting functions, not further discussed in this C++ book.
The streams working with fixed buffer arrays allow you to use the stream metaphor on any block of memory, independently of how memory for that buffer was allocated.
The rest of this section deals with console streams (cin and cout). Examples of file, string, and fixed buffer array streams are provided later in this chapter. Other types of streams, such as printer output or network I/O, are often platform dependent, so they are not covered in this book.
Output with Streams
Section titled “Output with Streams”Output using streams is introduced in Chapter 1. This section briefly revisits some of the basics and introduces material that is more advanced.
Output Basics
Section titled “Output Basics”Output streams are defined in <ostream>. There is also <iostream>, which in turn includes the functionality for both input streams and output streams. <iostream> also declares all predefined stream instances: cout, cin, cerr, clog, and the wide versions.
The << operator is the simplest way to use output streams. C++ basic types, such as ints, pointers, doubles, and characters, can be output using <<. In addition, the C++ string class is compatible with <<, and C-style strings are properly output as well. The following are some examples of using <<:
int i { 7 };cout << i << endl;
char ch { 'a' };cout << ch << endl;
string myString { "Hello World." };cout << myString << endl;The output is as follows:
7aHello World.The cout stream is the built-in stream for writing to the console, or standard output. You can “chain” uses of << together to output multiple pieces of data. This is because operator<< returns a reference to the stream as its result, so you can immediately use << again on the same stream. Here is an example:
int j { 11 };cout << "The value of j is " << j << "!" << endl;The output is as follows:
The value of j is 11!C++ streams correctly parse C-style escape sequences, such as strings that contain \n. You can also use std::endl to start a new line. The difference between using \n and endl is that \n just starts a new line while endl also flushes the buffer. Watch out with endl because too many flushes might hurt performance. The following example uses endl to output and flush several lines of text with just one line of code:
cout << "Line 1" << endl << "Line 2" << endl << "Line 3" << endl;The output is as follows:
Line 1Line 2Line 3endl flushes the destination buffer, so use it judiciously in performance critical code, such as tight loops.
Member Functions of Output Streams
Section titled “Member Functions of Output Streams”The << operator is, without a doubt, the most useful part of output streams. However, there is additional functionality to be explored. If you look through the contents of <ostream>, you’ll see many lines of overloaded definitions of the << operator to support outputting all kinds of different data types. You’ll also find some useful public member functions.
put() and write()
Section titled “put() and write()”put() and write() are raw output member functions. Instead of taking an object or variable that has some defined behavior for output, put() accepts a single character, while write() accepts a character array. The data passed to these member functions is output as is, without any special formatting or processing. For example, the following code snippet shows how to output a C-style string to the console without using the << operator:
const char* test { "hello there" };cout.write(test, strlen(test));The next snippet shows how to write a single character to the console by using the put() member function:
cout.put('a');flush()
Section titled “flush()”When you write to an output stream, the stream does not necessarily write the data to its destination right away. Most output streams buffer, or accumulate, data instead of writing it out as soon as it comes in. This is usually done to improve performance. Certain stream destinations, such as files, are much more performant if data is written in larger blocks, instead of, for example, character by character. The stream flushes, or writes out, the accumulated data, when one of the following conditions occurs:
- An
endlmanipulator is encountered. - The stream goes out of scope and is destructed.
- The stream buffer is full.
- You explicitly tell the stream to flush its buffer.
- Input is requested from a corresponding input stream (that is, when you make use of
cinfor input,coutwill flush). In the section “File Streams,” you will learn how to establish this type of link.
One way to explicitly tell a stream to flush is to call its flush() member function, as in the following code:
cout << "abc";cout.flush(); // abc is written to the console.cout << "def";cout << endl; // def is written to the console.Handling Output Errors
Section titled “Handling Output Errors”Output errors can arise in a variety of situations. Perhaps you are trying to open a non-existing file. Maybe a disk error has prevented a write operation from succeeding, for example, because the disk is full. None of the streams’ code you have seen up until this point has considered these possibilities, mainly for brevity. However, it is vital that you address any error conditions that occur.
When a stream is in its normal usable state, it is said to be “good.” The good() member function can be called directly on a stream to determine whether the stream is currently good:
if (cout.good()) { cout << "All good" << endl;}good() provides an easy way to obtain basic information about the validity of the stream, but it does not tell you why the stream is unusable. There is a member function called bad() that provides a bit more information. If bad() returns true, it means that a fatal error has occurred (as opposed to any nonfatal condition like end-of-file, eof()). Another member function, fail(), returns true if the most recent operation has failed; however, it doesn’t say anything about the next operation, which can either succeed or fail as well. For example, after calling flush() on an output stream, you could call fail() to make sure the flush was successful:
cout.flush();if (cout.fail()) { cerr << "Unable to flush to standard out" << endl;}Streams have a conversion operator to convert to type bool. This conversion operator returns the same as calling !fail(). So, the previous code snippet can be rewritten as follows:
cout.flush();if (!cout) { cerr << "Unable to flush to standard out" << endl;}Important to know is that both good() and fail() return false if the end-of-file is reached. The relation is as follows: good() == (!fail() && !eof()).
You can also tell the streams to throw exceptions when a failure occurs. You then write a catch handler to catch ios_base::failure exceptions, on which you can use the what() member function to get a description of the error, and the code() member function to get the error code. However, whether or not you get useful information depends on the Standard Library implementation that you use.
cout.exceptions(ios::failbit | ios::badbit | ios::eofbit);try { cout << "Hello World." << endl;} catch (const ios_base::failure& ex) { cerr << "Caught exception: " << ex.what() << ", error code = " << ex.code() << endl;}To reset the error state of a stream, use clear():
cout.clear();Error checking is performed less frequently for console output streams than for file output or input streams. The member functions discussed here apply for other types of streams as well and are revisited later as each type is discussed.
Output Manipulators
Section titled “Output Manipulators”One of the unusual features of streams is that you can throw more than just data down the chute. C++ streams also recognize manipulators, objects that make a change to the behavior of the stream instead of, or in addition to, providing data for the stream to work with.
You have already seen one manipulator: endl. The endl manipulator encapsulates data and behavior. It tells the stream to output an end-of-line sequence and to flush its buffer. The following is a non-exhaustive list of some other useful manipulators, many of which are defined in <ios> and <iomanip>. An example after this list shows how to use them:
boolalphaandnoboolalpha: Tells the stream to outputboolvalues as true and false (boolalpha) or 1 and 0 (noboolalpha). The default isnoboolalpha.hex,oct, anddec: Outputs numbers in hexadecimal, octal, and base 10, respectively.fixed,scientific, anddefaultfloat: Outputs fractional numbers using fixed, scientific, or default formatting, respectively.setprecision: Sets the number of decimal places that are output for fractional numbers using fixed or scientific formatting, or else the total number of digits to output. This is a parameterized manipulator (meaning that it takes an argument).setw: Sets the field width for outputting data. This is a parameterized manipulator.setfill: Sets a character as the new fill character for the stream. The fill character pads output according to the width set bysetw. This is a parameterized manipulator.showpointandnoshowpoint: Forces the stream to always or never show the decimal point for floating-point numbers with no fractional part.put_money: A parameterized manipulator that writes a formatted monetary value to a stream.put_time: A parameterized manipulator that writes a formatted time to a stream.quoted: A parameterized manipulator that encloses a given string with quotes and escapes embedded quotes.
All of these manipulators stay in effect for subsequent output to the stream until they are reset, except setw, which is active for only the next single output. The following example uses several of these manipulators to customize its output:
// Boolean valuesbool myBool { true };cout << "This is the default: " << myBool << endl;cout << "This should be true: " << boolalpha << myBool << endl;cout << "This should be 1: " << noboolalpha << myBool << endl;
// Simulate println-style "{:6}" with streamsint i { 123 };println("This should be ' 123': {:6}", i);cout << "This should be ' 123': " << setw(6) << i << endl;
// Simulate println-style "{:0>6}" with streamsprintln("This should be '000123': {:0>6}", i);cout << "This should be '000123': " << setfill('0') << setw(6) << i << endl;
// Fill with *cout << "This should be '***123': " << setfill('*') << setw(6) << i << endl;// Reset fill charactercout << setfill(' ');
// Floating-point valuesdouble dbl { 1.452 };double dbl2 { 5 };cout << "This should be ' 5': " << setw(2) << noshowpoint << dbl2 << endl;cout << "This should be @@1.452: " << setw(7) << setfill('@') << dbl << endl;// Reset fill charactercout << setfill(' ');
// Instructs cout to start formatting numbers according to your location.// Chapter 21 explains the details of the imbue() call and the locale object.cout.imbue(locale { "" });
// Format numbers according to your locationcout << "This is 1234567 formatted according to your location: " << 1234567 << endl;
// Monetary value. What exactly a monetary value means depends on your// location. For example, in the USA, a monetary value of 120000 means 120000// dollar cents, which is 1200.00 dollars.cout << "This should be a monetary value of 120000, " << "formatted according to your location: " << put_money("120000") << endl;
// Date and timetime_t t_t { time(nullptr) }; // Get current system time.tm t { *localtime(&t_t) }; // Convert to local time.cout << "This should be the current date and time " << "formatted according to your location: " << put_time(&t, "%c") << endl;
// Quoted stringcout << "This should be: \"Quoted string with \\\"embedded quotes\\\".\": " << quoted("Quoted string with \"embedded quotes\".") << endl;If you don’t care for the concept of manipulators, you can usually get by without them. Streams provide much of the same functionality through equivalent member functions like precision(). For example, take the following line:
cout << "This should be '1.2346': " << setprecision(5) << 1.23456789 << endl;This can be converted to use a member function call as follows. The advantage of the member function calls is that they return the previous value, allowing you to restore it, if needed.
cout.precision(5);cout << "This should be '1.2346': " << 1.23456789 << endl;For a detailed description of all stream member functions and manipulators, consult your favorite Standard Library Reference.
Input with Streams
Section titled “Input with Streams”Input streams provide a simple way to read in structured or unstructured data. In this section, the techniques for input are discussed within the context of cin, the console input stream.
Input Basics
Section titled “Input Basics”There are two easy ways to read data by using an input stream. The first is an analog of the << operator that outputs data to an output stream. The corresponding operator for reading data is >>. When you use >> to read data from an input stream, the variable you provide is the storage for the received value. For example, the following program reads one word from the user and puts it into a string. Then the string is output back to the console:
string userInput;cin >> userInput;println("User input was {}.", userInput);By default, the >> operator tokenizes values according to whitespace. For example, if a user runs the previous program and enters hello there as input, only the characters up to the first whitespace character (the space character in this instance) are captured into the userInput variable. The output would be as follows:
User input was hello.One solution to include whitespace in the input is to use get(), which is discussed later in this chapter.
The >> operator works with different variable types, just like the << operator. For example, to read an integer, the code differs only in the type of the variable:
int userInput;cin >> userInput;println("User input was {}.", userInput);You can use input streams to read in multiple values, mixing and matching types as necessary. For example, the following function, an excerpt from a restaurant reservation system, asks the user for a last name and the number of people in their party:
void getReservationData(){ string guestName; int partySize; print("Name and number of guests: "); cin >> guestName >> partySize; println("Thank you, {}.", guestName); if (partySize > 10) { println("An extra gratuity will apply."); }}Remember that the >> operator tokenizes values according to whitespace, so getReservationData() does not allow you to enter a name with whitespace. A solution using unget() is discussed later in this chapter. Note also that the first use of cout does not explicitly flush the buffer using endl or flush(), but still, the text will be written to the console because the use of cin immediately flushes the cout buffer; they are linked together in this way.
Handling Input Errors
Section titled “Handling Input Errors”Input streams have a number of member functions to detect unusual circumstances. Most of the error conditions related to input streams occur when there is no data available to read. For example, the end of the stream (referred to as end-of-file, even for non–file streams) may have been reached. The most common way of querying the state of an input stream is to access it within a conditional statement. For example, the following loop keeps looping as long as cin remains in a good state. This pattern takes advantage of the fact that evaluating an input stream within a conditional context results in true only if the stream is not in any error state. Encountering an error causes the stream to evaluate to false. The underlying details of the conversion operations required to implement such behavior are explained in Chapter 15, “Overloading C++ Operators.”
while (cin) { … }You can input data at the same time:
while (cin >> ch) { … }The good(), bad(), and fail() member functions can be called on input streams, just as on output streams. There is also an eof() member function that returns true if the stream has reached its end. Similar as for output streams, both good() and fail() return false if the end-of-file is reached. The relation is again as follows: good() == (!fail() && !eof()).
You should get into the habit of checking the stream state after reading data so that you can recover from bad input.
The following program shows a common pattern for reading data from a stream and handling errors. The program reads numbers from standard input and displays their sum once end-of-file is reached. Note that in command-line environments, the end-of-file is indicated by the user typing a particular character. In Unix and Linux, it is Control+D; in Windows it is Control+Z, both followed by Enter. The exact character is operating-system dependent, so you will need to know what your operating system requires.
println("Enter numbers on separate lines to add.");println("Use Control+D followed by Enter to finish (Control+Z in Windows).");int sum { 0 };
if (!cin.good()) { println(cerr, "Standard input is in a bad state!"); return 1;}
while (!cin.bad()) { int number; cin >> number; if (cin.good()) { sum += number; } else if (cin.eof()) { break; // Reached end-of-file. } else if (cin.fail()) { // Failure! cin.clear(); // Clear the failure state. string badToken; cin >> badToken; // Consume the bad input. println(cerr, "WARNING: Bad input encountered: {}", badToken); }}println("The sum is {}.", sum);Here is some example output of this program. The ^Z characters in the output appear when Control+Z is pressed.
Enter numbers on separate lines to add.Use Control+D followed by Enter to finish (Control+Z in Windows).12testWARNING: Bad input encountered: test3^ZThe sum is 6.Input Member Functions
Section titled “Input Member Functions”Just like output streams, input streams have several member functions that allow a lower level of access than the functionality provided by the more common >> operator.
The get() member function allows raw input of data from a stream. The simplest version of get() returns the next character in the stream, though other versions exist that read multiple characters at once. get() is most commonly used to avoid the automatic tokenization that occurs with the >> operator. For example, the following function reads a name, which can be made up of several words, from an input stream until the end of the stream is reached:
string readName(istream& stream){ string name; while (stream) { // Or: while (!stream.fail()) { int next { stream.get() }; if (!stream || next == std::char_traits<char>::eof()) break; name += static_cast<char>(next);// Append character. } return name;}There are several interesting observations to make about this readName() function:
- Its parameter is a reference-to-non-
constto anistream, not a reference-to-const. The member functions that read in data from a stream will change the actual stream (most notably, its position), so they are notconstmember functions. Thus, you cannot call them on a reference-to-const. - The return value of
get()is stored in anint, not in achar, becauseget()can return special non-character values such asstd::char_traits<char>::eof()(end-of-file). - Newline and other escape characters that are read by
get()will appear in thestringreturned byreadName(). If theCtrl+DorCtrl+Zisn’t done at the beginning of a line, they too will appear in the returnedstring.
readName() is a bit strange because there are two ways to get out of the loop: either the stream can get into a failed state or the end of the stream is reached. A more common pattern for reading from a stream uses a different version of get() that takes a reference to a character and returns a reference to the stream. Evaluating an input stream within a conditional context results in true only if the stream is not in any error state. The following version of the same function is a bit more concise:
string readName(istream& stream){ string name; char next; while (stream.get(next)) { name += next; } return name;}unget()
Section titled “unget()”For most purposes, the correct way to think of an input stream is as a one-way chute. Data falls down the chute and into variables. The unget() member function breaks this model in a way by allowing you to push data back up the chute.
A call to unget() causes the stream to back up by one position, essentially putting the previous character read back on the stream. You can use the fail() member function to see whether unget() was successful or not. For example, unget() can fail if the current position is at the beginning of the stream.
The getReservationData() function shown earlier in this chapter did not allow you to enter a name with whitespace. The following code uses unget() to allow whitespace in the name. The code reads character by character and checks whether the character is a digit or not. If the character is not a digit, it is added to guestName. If it is a digit, the character is put back into the stream using unget(), the loop is stopped, and the >> operator is used to input an integer, partySize. The noskipws input manipulator tells the stream not to skip whitespace; that is, whitespace is read like any other characters.
void getReservationData(){ print("Name and number of guests: "); string guestName; int partySize { 0 }; // Read characters until we find a digit char ch; cin >> noskipws; while (cin >> ch) { if (isdigit(ch)) { cin.unget(); if (cin.fail()) { println(cerr, "unget() failed."); } break; } guestName += ch; } // Read partySize, if the stream is not in error state if (cin) { cin >> partySize; } if (!cin) { println(cerr, "Error getting party size."); return; }
println("Thank you '{}', party of {}.", guestName, partySize); if (partySize > 10) { println("An extra gratuity will apply."); }}putback()
Section titled “putback()”The putback() member function, like unget(), lets you move backward by one character in an input stream. The difference is that putback() takes the character being placed back on the stream as a parameter. Here is an example:
char c;cin >> c;println("Retrieved {}.", c);
cin.putback('e'); // 'e' will be the next character read off the stream.println("Called putback('e').");
while (cin >> c) { println("Retrieved {}.", c); }The output can be as follows:
wowRetrieved w.Called putback('e').Retrieved e.Retrieved o.Retrieved w.peek ()
Section titled “peek ()”The peek() member function allows you to preview the next value that would be returned if you were to call get(). To take the chute metaphor perhaps a bit too far, you could think of it as looking up the chute without a value actually falling down it.
peek() is ideal for any situation where you need to look ahead before reading a value. For example, the following code implements getReservationData() that allows whitespace in the name, but uses peek() instead of unget():
void getReservationData(){ print("Name and number of guests: "); string guestName; int partySize { 0 }; // Read characters until we find a digit. cin >> noskipws; while (true) { // 'peek' at next character. char ch { static_cast<char>(cin.peek()) }; if (!cin) { break; } if (isdigit(ch)) { // Next character will be a digit, so stop the loop. break; } // Next character will be a non-digit, so read it. cin >> ch; if (!cin) { break; } guestName += ch; } // Read partySize, if the stream is not in error state. if (cin) { cin >> partySize; } if (!cin) { println(cerr, "Error getting party size."); return; }
println("Thank you '{}', party of {}.", guestName, partySize); if (partySize > 10) { println("An extra gratuity will apply."); }}getline()
Section titled “getline()”Obtaining a single line of data from an input stream is so common that a member function exists to do it for you. The getline() member function fills a character buffer with a line of data up to the specified size. This specified size includes the \0 character. Thus, the following code reads a maximum of BufferSize-1 characters from cin or until an end-of-line sequence is read:
char buffer[BufferSize] { 0 };cin.getline(buffer, BufferSize);When getline() is called, it reads a line from the input stream, up to and including the end-of-line sequence. However, the end-of-line character or characters do not appear in the string. Note that the end-of-line sequence is platform dependent. For example, it can be \r\n, \n, or \n\r.
There is a form of get() that performs the same operation as getline(), except that it leaves the newline sequence in the input stream.
There is also a non-member function called std::getline() that can be used with C++ strings. It is defined in <string> and is in the std namespace. It takes a stream and a string reference. The advantage of using this version of getline() is that it doesn’t require you to specify the size of any buffer.
string myString;getline(cin, myString);Both the getline() member function and the std::getline() function accept an optional delimiter as last parameter. The default delimiter is \n. By changing this delimiter, these functions can be used to read in multiple lines of text until a given delimiter is reached. For example, the following code reads in multiple lines of text until it reads an @ character:
print("Enter multiple lines of text. " "Use an @ character to signal the end of the text.\n> ");string myString;getline(cin, myString, '@');println("Read text: \"{}\"", myString);Here is a possible output:
Enter multiple lines of text. Use an @ character to signal the end of the text.> This is sometext on multiplelines.@Read text: "This is sometext on multiplelines."Input Manipulators
Section titled “Input Manipulators”Just as for output streams, input streams support a number of input manipulators. You’ve already seen one, noskipws, which tells an input stream not to skip any whitespace characters. The following list shows other built-in input manipulators available to you that allow you to customize the way data is read:
boolalphaandnoboolalpha: Ifboolalphais used, the string false will be interpreted as the Boolean valuefalse; anything else will be treated as the Boolean valuetrue. Ifnoboolalphais set, zero will be interpreted asfalse, anything else astrue. The default isnoboolalpha.dec,hex, andoct: Reads numbers in decimal base 10, hexadecimal, or octal notation, respectively. For example, the decimal base 10 number207iscfin hexadecimal, and317in octal notation.skipwsandnoskipws: Tells the stream to either skip whitespace when tokenizing or to read in whitespace as its own token. The default isskipws.ws: A handy manipulator that simply skips over the current series of whitespace at the current position in the stream.get_money: A parameterized manipulator that reads a monetary value from a stream.get_time: A parameterized manipulator that reads a formatted time from a stream.quoted: A parameterized manipulator that reads a string enclosed with quotes and in which embedded quotes are escaped. An example of this manipulator is shown later in this chapter.
Input is locale aware. For example, the following code enables your system locale for cin. Locales are discussed in Chapter 21:
cin.imbue(locale { "" });int i;cin >> i;If your system locale is U.S. English, you can enter 1,000 and it will be parsed as 1000. If you enter 1.000, it will be parsed as 1. On the other hand, if your system locale is Dutch Belgium, you can enter 1.000 to get the value of 1000, but entering 1,000 will result in 1. In both cases, you can also just enter 1000 without any digit separators to get the value 1000.
Input and Output with Objects
Section titled “Input and Output with Objects”If you are familiar with the old-school printf() function from C for output, you know that it is not flexible and does not support custom types. printf() knows about several types of data, but there really isn’t a way to give it additional knowledge. For example, consider the following simple class:
class Muffin final{ public: const string& getDescription() const { return m_description; } void setDescription(string description) { m_description = std::move(description); }
int getSize() const { return m_size; } void setSize(int size) { m_size = size; }
bool hasChocolateChips() const { return m_hasChocolateChips; } void setHasChocolateChips(bool hasChips) { m_hasChocolateChips = hasChips; } private: string m_description; int m_size { 0 }; bool m_hasChocolateChips { false };};To output an object of class Muffin by using printf(), it would be nice if you could specify it as an argument, perhaps using %m as a placeholder:
printf("Muffin: %m\n", myMuffin); // BUG! printf doesn't understand Muffin.Unfortunately, the printf() function knows nothing about the Muffin type and is unable to output an object of type Muffin. Worse still, because of the way the printf() function is declared, this will result in a run-time error, not a compile-time error (though a good compiler will give you a warning).
The best you can do with printf() is to add a new output() member function to the Muffin class:
class Muffin final{ public: void output() const { printf("%s, size is %d, %s", getDescription().c_str(), getSize(), (hasChocolateChips() ? "has chips" : "no chips")); } // Omitted for brevity};Using such a mechanism is cumbersome, however. To output a Muffin in the middle of another line of text, you’d need to split the line into two calls with a call to Muffin::output() in between, as shown here:
printf("The muffin is a ");myMuffin.output();printf(" -- yummy!\n");A much better and modern option is to write a custom std::formatter specialization for Muffin objects as explained in Chapter 2. The following is a simple custom formatter for Muffins. To keep the example concise, this formatter does not support any format specifiers. Thus, the parse() abbreviated member function template does not need to parse anything and can just return begin(context).
template <>class std::formatter<Muffin>{ public: constexpr auto parse(auto& context) { return begin(context); }
auto format(const Muffin& muffin, auto& ctx) const { ctx.advance:to(format_to(ctx.out(), "{}, size is {}, {}", muffin.getDescription(), muffin.getSize(), (muffin.hasChocolateChips() ? "has chips" : "no chips"))); return ctx.out(); }};With this custom formatter, you can use the modern std::print() and println() functions to print a muffin. Here is an example:
println("The muffin is a {} -- yummy!", myMuffin);Yet another option to print Muffins is to overload the << operator after which you output a Muffin just like you output a string—by providing it as an argument to operator<<. Additionally, you can overload the >> operator so that you can input Muffins from an input stream. Chapter 15 covers the details of overloading the << and >> operators.
Custom Manipulators
Section titled “Custom Manipulators”The Standard Library comes with a number of built-in stream manipulators, but, if need be, custom manipulators can be written.
Writing your own non-parameterized manipulator is easy. Here’s a simple example defining a tab output manipulator which outputs the tab character to a given stream:
ostream& tab(ostream& stream) { return stream << '\t'; }
int main(){ cout << "Test" << tab << "!" << endl;}Writing custom parameterized manipulators is more complicated. It involves using functionality exposed by ios_base, such as xalloc(), iword(), pword(), and register_callback(). Since such manipulators are rarely needed, this text does not further discuss this topic. Consult your favorite Standard Library reference in case you are interested.
STRING STREAMS
Section titled “STRING STREAMS”String streams provide a way to use stream semantics with strings. In this way, you can have an in-memory stream that represents textual data. For example, in a GUI application you might want to use streams to build up textual data, but instead of outputting the text to the console or a file, you might want to display the result in a GUI element like a message box or an edit control. Another example could be that you want to pass a string stream around to different functions, while retaining the current read position, so that each function can process the next part of the stream. String streams are also useful for parsing text, because streams have built-in tokenizing functionality.
The std::ostringstream class is used to write data to a string, while std::istringstream is used to read data from a string. The o in ostringstream stands for output, while the i in istringstream stands for input. They are both defined in <sstream>. Because ostringstream and istringstream inherit the same behavior as ostream and istream, working with them is pleasantly similar.
The following program requests words from the user and outputs them to a single ostringstream, separated by commas and surrounded by double quotes. At the end of the program, the whole stream is turned into a string object using the str() member function and is written to the console. Input of tokens can be stopped by entering the token “done” or by closing the input stream with Control+D (Unix) or Control+Z (Windows).
println("Enter tokens. " "Control+D (Unix) or Control+Z (Windows) followed by Enter to end.");ostringstream outStream;bool firstLoop { true };while (cin) { string nextToken; print("Next token: "); cin >> nextToken;
if (!cin || nextToken == "done") { break; }
if (!firstLoop) { outStream << ", "; } outStream << '"' << nextToken << '"'; firstLoop = false;}println("The end result is: {}", outStream.str());Reading data from a string stream is similarly familiar. The following function creates and populates a Muffin object (see the earlier example) from a string input stream. The stream data is in a fixed format so that the function can easily turn its values into calls to Muffin’s setters. This fixed format is the description of the muffin between double quotes, followed by the size, followed by true or false depending on whether the muffin has chocolate chips. For example, the following string is a valid muffin:
"Raspberry Muffin" 12 trueHere is the implementation. Note the use of the quoted manipulator to read a quoted string from the input stream.
Muffin createMuffin(istringstream& stream){ Muffin muffin; // Assume data is properly formatted: // "Description" size chips
string description; int size; bool hasChips;
// Read all three values. Note that chips is represented // by the strings "true" and "false". stream >> quoted(description) >> size >> boolalpha >> hasChips; if (stream) { // Reading was successful. muffin.setSize(size); muffin.setDescription(description); muffin.setHasChocolateChips(hasChips); } return muffin;}An advantage of string streams over standard C++ strings is that, in addition to data, they know where the next read or write operation will take place, also called the current position.
Another advantage is that string streams support manipulators and locales to enable more powerful formatting compared to strings.
Finally, if you need to build up a string by concatenating several smaller strings, using a string stream will be more performant compared to concatenating string objects directly.
SPAN-BASED STREAMS
Section titled “ SPAN-BASED STREAMS”C++23 introduces span-based streams, defined in <spanstream>, which allow you to use the stream metaphor on any fixed memory buffer you have available. How memory was allocated for that buffer is not important. The most important classes in this context that you’ll use are ispanstream for input, ospanstream for output, and spanstream for input and output. Technically, these are char instantiations for the class templates basic_ispanstream, basic_ospanstream, and basic_spanstream. There are also wide-character, wchar_t instantiations available called wispanstream, wospanstream, and wspanstream. Wide characters are mentioned earlier in this chapter and covered in more detail in Chapter 21. This section gives examples of the non-wide-character classes, as the others work very similarly.
The constructors of the span-based stream classes require an std::span. Chapter 18, “Standard Library Containers,” discusses span in detail and explains why and when you want to use it, but those details are not important for this section. The use of span in the context of span-based streams is straightforward, as you’ll see. In a nutshell, a span allows you to make a view over a contiguous block of memory. It’s a bit similar to how std::string_view allows you to create a read-only view over any kind of string, as discussed in Chapter 2. The difference is that a span can be a read-only view, but it can also be a writable view allowing modifications to the underlying buffer.
Here is an example of using an ispanstream to parse data stored in a fixed memory buffer called fixedBuffer. To construct a span over that buffer, you simply use the span constructor and pass it the location of the buffer.
char fixedBuffer[] { "11 2.222 Hello" };ispanstream stream { span { fixedBuffer } };int i; double d; string str;stream >> i >> d >> str;println("Parsed data: int: {}, double: {}, string: {}", i, d, str);The output is as follows:
Parsed data: int: 11, double: 2.222, string: HelloUsing an ospanstream is similarly straightforward. The following code creates a fixed buffer of 32 chars, constructs a writable ospanstream view over that buffer, uses standard stream insertion operations to output some data to the buffer, and finally prints out the result:
char fixedBuffer[32] {};ospanstream stream { span { fixedBuffer } };stream << "Hello " << 2.222 << ' ' << 11;println("Buffer contents: \"{}\"", fixedBuffer);The output is:
Buffer contents: "Hello 2.222 11"FILE STREAMS
Section titled “FILE STREAMS”Files lend themselves well to the stream abstraction because reading and writing files always involves a position in addition to the data. In C++, the std::ofstream and ifstream classes provide output and input functionality for files. They are defined in <fstream>.
When dealing with the filesystem, it is especially important to detect and handle error cases. The file you are working with could be on a network file store that just went offline, or you may be trying to write to a file that is located on a disk that is full. Maybe you are trying to open a file for which the current user does not have permissions. Error conditions can be detected by using the standard error handling mechanisms described earlier.
The only major difference between output file streams and other output streams is that the file stream constructor can take the name of the file and the mode in which you would like to open it. The default mode is write, ios_base::out, which starts writing to a file at the beginning, overwriting any existing data. You can also open an output file stream in append mode by specifying the constant ios_base::app as the second argument to the file stream constructor. The following table lists the different constants that are available:
| CONSTANT | DESCRIPTION |
|---|---|
| ios_base::app | Open, and go to the end before each write operation. |
| ios_base::ate | Open, and go to the end once immediately after opening. |
| ios_base::binary | Perform input and output in binary mode as opposed to text mode. See the next section. |
| ios_base::in | Open for input, start reading at the beginning. |
| ios_base::out | Open for output, start writing at the beginning, overwriting existing data. |
| ios_base::trunc | Option for out. Delete all existing data (truncate). |
Option for out. Open in exclusive mode. Open will fail if the file already exists. |
Note that modes can be combined. For example, if you want to open a file for output in binary mode, while truncating existing data, you would specify the open mode as follows:
ios_base::out | ios_base::binary | ios_base::truncAn ifstream automatically includes the ios_base::in mode, while an ofstream automatically includes the ios_base::out mode, even if you don’t explicitly specify in or out as the mode.
The following program opens the test.txt file and writes the program’s arguments to it. The ifstream and ofstream destructors automatically close the underlying file, so there is no need to explicitly call close().
int main(int argc, char* argv[]){ ofstream outFile { "test.txt", ios_base::trunc }; if (!outFile.good()) { println(cerr, "Error while opening output file!"); return -1; } outFile << "There were " << argc << " arguments to this program." << endl; outFile << "They are: " << endl; for (int i { 0 }; i < argc; i++) { outFile << argv[i] << endl; }}Text Mode vs. Binary Mode
Section titled “Text Mode vs. Binary Mode”By default, a file stream is opened in text mode. If you specify the ios_base::binary flag, then the file is opened in binary mode.
In binary mode, the exact bytes you ask the stream to write are written to the file. When reading, the bytes are returned to you exactly as they are in the file.
In text mode, there is some hidden conversion happening: each line you write to, or read from, a file ends with \n. However, how the end of a line is encoded in a file depends on the operating system. For example, on Windows, a line ends with \r\n instead of with a single \n character. Therefore, when a file is opened in text mode and you write a line ending with \n to it, the underlying implementation automatically converts the \n to \r\n before writing it to the file. Similarly, when reading a line from the file, the \r\n that is read from the file is automatically converted back to \n before being returned to you.
Jumping Around with seek() and tell()
Section titled “Jumping Around with seek() and tell()”The seekx() and tellx() member functions are present on all input and output streams. The seekx() member functions let you move to an arbitrary position within an input or output stream. There are several forms of seekx(). For an input stream, the member function is called seekg() (the g is for get), and for an output stream, it is called seekp() (the p is for put). You might wonder why there is both a seekg() and a seekp() member function, instead of one seek() member function. The reason is that you can have streams that are both input and output, for example, file streams. In that case, the stream needs to remember both a read position and a separate write position. This is also called bidirectional I/O and is covered later in this chapter.
There are two overloads of seekg() and two of seekp(). One overload accepts a single argument, an absolute position, and seeks to this absolute position. The second overload accepts an offset and a position and seeks an offset relative to the given position. Positions are of type std::streampos, while offsets are of type std::streamoff; both are measured in bytes. There are three predefined positions available, as shown in the following table:
| POSITION | DESCRIPTION |
|---|---|
| ios_base::beg | The beginning of the stream |
| ios_base::end | The end of the stream |
| ios_base::cur | The current position in the stream |
For example, to seek to an absolute position in an output stream, you can use the one-parameter overload of seekp(), as in the following case, which uses the constant ios_base::beg to move to the beginning of the stream:
outStream.seekp(ios_base::beg);Seeking within an input stream is the same, except that the seekg() member function is used:
inStream.seekg(ios_base::beg);The two-argument overloads move to a relative position in the stream. The first argument prescribes how many positions to move, and the second argument provides the starting point. To move relative to the beginning of the file, the constant ios_base::beg is used. To move relative to the end of the file, ios_base::end is used. To move relative to the current position, ios_base::cur is used. For example, the following line moves to the second byte from the beginning of the output stream. Note that integers are implicitly converted to type streampos and streamoff.
outStream.seekp(2, ios_base::beg);The next example moves to the third-to-last byte of an input stream:
inStream.seekg(-3, ios_base::end);You can also query a stream’s current location using the tellx() member function, which returns a streampos that indicates the current position. You can use this result to remember the current marker position before doing a seekx() or to query whether you are in a particular location. There are again separate versions of tellx() for input streams and output streams. Input streams use tellg(), and output streams use tellp().
The following code checks the position of an input stream to determine whether it is at the beginning:
streampos curPos { inStream.tellg() };if (ios_base::beg == curPos) { println("We're at the beginning.");}The following is a sample program that brings it all together. This program writes into a file called test.out and performs the following tests:
- Outputs the string
54321to the file - Verifies that the marker is at position 5 in the stream
- Moves to position 2 in the output stream
- Outputs a
0in position 2 and closes the output stream - Opens an input stream on the
test.outfile - Reads the first token as an integer
- Confirms that the value is
54021
ofstream fout { "test.out" };if (!fout) { println(cerr, "Error opening test.out for writing."); return 1;}
// 1. Output the string "54321".fout << "54321";
// 2. Verify that the marker is at position 5.streampos curPos { fout.tellp() };if (curPos == 5) { println("Test passed: Currently at position 5.");} else { println("Test failed: Not at position 5!");}
// 3. Move to position 2 in the output stream.fout.seekp(2, ios_base::beg);
// 4. Output a 0 in position 2 and close the output stream.fout << 0;fout.close();
// 5. Open an input stream on test.out.ifstream fin { "test.out" };if (!fin) { println(cerr, "Error opening test.out for reading."); return 1;}
// 6. Read the first token as an integer.int testVal;fin >> testVal;if (!fin) { println(cerr, "Error reading from file."); return 1;}
// 7. Confirm that the value is 54021.const int expected { 54021 };if (testVal == expected) { println("Test passed: Value is {}.", expected);} else { println("Test failed: Value is not {} (it was {}).", expected, testVal);}Linking Streams Together
Section titled “Linking Streams Together”A link can be established between any input and output streams to give them flush-on-access behavior. In other words, when data is requested from an input stream, its linked output stream is automatically flushed. This behavior is available to all streams but is particularly useful for file streams that may be dependent upon each other.
Stream linking is accomplished with the tie() member function. To tie an output stream to an input stream, call tie() on the input stream, and pass the address of the output stream. To break the link, pass nullptr.
The following program ties the input stream of one file to the output stream of an entirely different file. You could also tie it to an output stream on the same file, but bidirectional I/O (covered in the next section) is perhaps a more elegant way to read and write the same file simultaneously.
ifstream inFile { "input.txt" }; // Note: input.txt must exist.ofstream outFile { "output.txt" };// Set up a link between inFile and outFile.inFile.tie(&outFile);// Output some text to outFile. Normally, this would// not flush because std::endl is not send.outFile << "Hello there!";// outFile has NOT been flushed.// Read some text from inFile. This will trigger flush() on outFile.string nextToken;inFile >> nextToken;// outFile HAS been flushed.The flush() member function is defined on the ostream base class, so you can also link an output stream to another output stream. Here’s an example:
outFile.tie(&anotherOutputFile);Such a relationship means that every time you write to one file, the buffered data that has been sent to the other file is flushed. You can use this mechanism to keep two related files synchronized.
One example of this stream linking is the link between cout and cin. Whenever you try to input data from cin, cout is automatically flushed. There is also a link between cerr and cout, meaning that any output to cerr causes cout to flush. The clog stream, on the other hand, is not linked to cout. The wide versions of these streams have similar links.
Read an Entire File
Section titled “Read an Entire File”You can use getline() to read the entire contents of a file by specifying \0 as the delimiter. This works only as long as the file doesn’t contain any \0 characters in its contents. For example:
ifstream inputFile { "some_data.txt" };if (inputFile.fail()) { println(cerr, "Unable to open file for reading."); return 1;}string fileContents;getline(inputFile, fileContents, '\0');println("\"{}\"", fileContents);BIDIRECTIONAL I/O
Section titled “BIDIRECTIONAL I/O”So far, this chapter has discussed input and output streams as two separate but related classes. However, there is such a thing as a stream that performs both input and output: a bidirectional stream.
Bidirectional streams derive from iostream, which in turn derives from both istream and ostream, thus serving as an example of useful multiple inheritance. As you would expect, bidirectional streams support both the >> operator and the << operator, as well as the member functions of both input streams and output streams.
The fstream class provides a bidirectional file stream. fstream is ideal for applications that need to replace data within a file, because you can read until you find the correct position and then immediately switch to writing. For example, imagine a program that stores a list of mappings between ID numbers and phone numbers. It might use a data file with the following format:
123 408-555-0394124 415-555-3422263 585-555-3490100 650-555-3434A reasonable approach to such a program would be to read in the entire data file when the program opens and rewrite the file, with any modifications, when the program closes. If the data set is huge, however, you might not be able to keep everything in memory. With iostreams, you don’t have to. You can easily scan through the file to find a record, and you can add new records by opening the file for output in append mode. To modify an existing record, you could use a bidirectional stream, as in the following function that changes the phone number for a given ID:
bool changeNumberForID(const string& filename, int id, string_view newNumber){ fstream ioData { filename }; if (!ioData) { println(cerr, "Error while opening file {}.", filename); return false; }
// Loop until the end of file. while (ioData) { // Read the next ID. int idRead; ioData >> idRead; if (!ioData) { break; }
// Check to see if the current record is the one being changed. if (idRead == id) { // Seek the write position to the current read position. ioData.seekp(ioData.tellg()); // Output a space, then the new number. ioData << " " << newNumber; break; }
// Read the current number to advance the stream. string number; ioData >> number; } return true;}Of course, an approach like this only works properly if the data is of a fixed size. When the preceding program switched from reading to writing, the output data overwrote other data in the file. To preserve the format of the file and to avoid writing over the next record, the data had to be the exact same size.
String streams can also be accessed in a bidirectional manner through the stringstream class.
FILESYSTEM SUPPORT LIBRARY
Section titled “FILESYSTEM SUPPORT LIBRARY”The C++ Standard Library includes a filesystem support library, defined in <filesystem> and living in the std::filesystem namespace. It allows you to write portable code to work with the filesystem. You can use it for querying whether something is a directory or a file, iterating over the contents of a directory, manipulating paths, and retrieving information about files such as their size, extension, creation time, and so on. The two most important parts of the library—paths and directory entries—are introduced in the next sections.
The basic component of the library is a path. A path can be absolute or relative and can optionally include a filename. For example, the following code defines a couple of paths. Note the use of raw string literals, introduced in Chapter 2, to avoid having to escape backslashes:
path p1 { R"(D:\Foo\Bar)" };path p2 { "D:/Foo/Bar" };path p3 { "D:/Foo/Bar/MyFile.txt" };path p4 { R"(..\SomeFolder)" };path p5 { "/usr/lib/X11" };A path can be converted to the native format of the system on which the code is running by calling string(). Here’s an example:
println("{}", p1.string());println("{}", p2.string());The output on Windows, which supports both forward and backward slashes, is as follows:
D:\Foo\BarD:/Foo/BarYou can append a component to a path with the append() member function or with operator/=. A platform-dependent path separator is automatically inserted. Here’s an example:
path p { "D:\\Foo" };p.append("Bar");p /= "Bar";println("{}", p.string());The output on Windows is D:\Foo\Bar\Bar.
You can use concat() or operator+= to concatenate a string to an existing path. This does not insert any path separator! Here’s an example:
path p { "D:\\Foo" };p.concat("Bar");p += "Bar";println("{}", p.string());The output on Windows now is D:\FooBarBar.
append() and operator/= automatically insert a platform-dependent path separator, while concat() and operator+= do not.
A range-based for loop can be used to iterate over the different components of a path. Here is an example:
path p { R"(C:\Foo\Bar)" };for (const auto& component : p) { println("{}", component.string());}The output on Windows is as follows:
C:\FooBarThe path interface supports operations such as remove_filename(), replace:filename(), replace:extension(), root_name(), parent_path(), extension(), stem(), filename(), has_extension(), is_absolute(), is_relative(), and more. A few of these are demonstrated in the following code snippet:
path p { R"(C:\Foo\Bar\file.txt)" };println("{}", p.root_name().string());println("{}", p.filename().string());println("{}", p.stem().string());println("{}", p.extension().string());This code produces the following result on Windows:
C:file.txtfile.txtConsult your favorite Standard Library reference for a full list of all available functionality.
Directory Entry
Section titled “Directory Entry”A path just represents a directory or a file on a filesystem. A path may refer to a non-existing directory or file. If you want to query an actual directory or file on the filesystem, you need to construct a directory_entry from a path. The directory_entry interface supports operations such as exists(), is_directory(), is_regular_file(), file_size(), last_write_time(), and others.
The following example constructs a directory_entry from a path to query the size of a file:
path myPath { "c:/windows/win.ini" };directory_entry dirEntry { myPath };if (dirEntry.exists() && dirEntry.is_regular_file()) { println("File size: {}", dirEntry.file_size());}Helper Functions
Section titled “Helper Functions”An entire collection of helper functions is available as well. For example, you can use copy() to copy files or directories, create_directory() to create a new directory on the filesystem, exists() to query whether a given directory or file exists, file_size() to get the size of a file, last_write_time() to get the time the file was last modified, remove() to delete a file, temp_directory_path() to get a directory suitable for storing temporary files, space() to query the available space on a filesystem, and more. Consult a Standard Library reference (see Appendix B, “Annotated Bibliography”) for a full list.
The following example prints out the capacity of a filesystem and how much space is still free:
space:info s { space("c:\\") };println("Capacity: {}", s.capacity);println("Free: {}", s.free);You can find more examples of these helper functions in the following section on directory iteration.
Directory Iteration
Section titled “Directory Iteration”If you want to recursively iterate over all files and subdirectories in a given directory, you can use a recursive_directory_iterator. To start the iteration process, you need an iterator to the first directory_entry. To know when to stop the iteration, you need an end iterator. To create the start iterator, construct a recursive_directory_iterator and pass as argument a path of the directory over which you want to iterate. To construct the end iterator, default construct a recursive_directory_iterator. To get access to the directory_entry that an iterator refers to, use the dereferencing operator, *. Traversing all elements in the collection is accomplished by simply incrementing the iterator using the ++ operator until it reaches the end iterator. Note that the end iterator is not part of the collection anymore and hence does not refer to a valid directory_entry and must not be dereferenced.
void printDirectoryStructure(const path& p){ if (!exists(p)) { return; }
recursive_directory_iterator begin { p }; recursive_directory_iterator end { }; for (auto iter { begin }; iter != end; ++iter) { const string spacer(iter.depth() * 2, ' ');
auto& entry { *iter }; // Dereference iter to access directory_entry.
if (is_regular_file(entry)) { println("{}File: {} ({} bytes)", spacer, entry.path().string(), file_size(entry)); } else if (is_directory(entry)) { println("{}Dir: {}", spacer, entry.path().string()); } }}This function can be called as follows:
path p { R"(D:\Foo\Bar)" };printDirectoryStructure(p);You can also use a directory_iterator to iterate over the contents of a directory and implement the recursion yourself. Here is an example that does the same thing as the previous example but using a directory_iterator instead of a recursive_directory_iterator:
void printDirectoryStructure(const path& p, unsigned level = 0){ if (!exists(p)) { return; }
const string spacer(level * 2, ' ');
if (is_regular_file(p)) { println("{}File: {} ({} bytes)", spacer, p.string(), file_size(p)); } else if (is_directory(p)) { println("{}Dir: {}", spacer, p.string()); for (auto& entry : directory_iterator { p }) { printDirectoryStructure(entry, level + 1); } }}SUMMARY
Section titled “SUMMARY”Streams provide a flexible and object-oriented way to perform input and output. The most important message in this chapter, even more important than the use of streams, is the concept of a stream. Some operating systems may have their own file access and I/O facilities, but knowledge of how streams and stream-like libraries work is essential to working with any type of modern I/O system.
The chapter finished with an introduction to the filesystem support library, which you can use to work with files and directories in a platform-independent manner.
EXERCISES
Section titled “EXERCISES”By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/proc++6e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website.
- Exercise 13-1: Let’s revisit the
Personclass you developed during exercises in previous chapters. Take your implementation from Exercise 9-2 and add anoutput()member function that writes the details of a person to the standard output console. - Exercise 13-2: The
output()member function from the previous exercise always writes the details of a person to the standard output console. Change theoutput()member function to have an output stream as parameter and write the details of a person to that stream. Test your new implementation inmain()by writing a person to the standard output console, a string stream, and a file. Notice how it’s possible to output a person to all kinds of different targets (output console, string streams, files, and so on) with a single member function using streams. - Exercise 13-3: Develop a class called
Databasethat storesPersons (from Exercise 13-2) in anstd::vector. Provide anadd()member function to add a person to the database. Also provide asave()member function, accepting the name of a file to which it saves all persons in the database. Any existing contents in the file is removed. Add aload()member function, accepting the name of a file from which the database loads all persons. Provide aclear()member function to remove all persons from the database. Finally, add a member functionoutputAll()that callsoutput()on all persons in the database. Make sure your implementation works, even if there are spaces in a person’s first or last name. - Exercise 13-4: The
Databasefrom Exercise 13-3 stores all persons in a single file. To practice the filesystem support library, let’s change that to store each person in its own file. Modify thesave()andload()member functions to accept a directory as argument where files should be stored to or loaded from. Thesave()member function saves every person in the database to its own file. The name of each file is the first name of the person followed by an underscore followed by the last name of the person. The extension of the files should be.person. If a file already exists, overwrite it. Theload()member function iterates over all.personfiles in a given directory and loads all of them.