14.10. Bulletproof Code (2): String Streams

Time: 00:04:58 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides: PDF, PPTX
Review

Programmers regard C++ as a strongly typed language, implying that all data have a type. The type determines how much memory the computer allocates to store the data and how it interprets the data's pattern of 1s and 0s. One problem programmers face is implementing robust or "bulletproof" data input that is resilient to incorrect user input. If the program can detect an input error, it can prevent it from causing a failure or data corruption. The program can easily detect out-of-bounds errors because in- and out-of-bounds values are usually the same type. For example, the program prompts the user for a double value to use as the sqrt function's argument and the user enters a negative value. The input operation succeeds, allowing the program to advance to the bounds check. But type conflicts can cause input operations to fail before programs can detect them.

Together, the characters in a string can represent any data, including currency and time designators. Consequently, programs solve the type conflict problem by reading input data as a string, validating it, and converting it to the appropriate type. The casting operator can convert one type to a similar one, like an integer and a double, but cannot convert a string to another type. Although C generally provided functions to convert between C-strings and numbers, they were not universally available because no governing standard required them. The ANSI C++ 2011 standard added the to_string functions to convert numbers to string objects. Before the C++11 standard, programmers used streams to convert between numbers and strings.

String Streams

The dual nature of string streams allows programs to use them both ways at different times. Programs can write data to an output stream using its intrinsic conversion operations to convert numbers to strings. Alternatively, programs can read data into a string stream like a string and later read the data out like a stream. The following figures summarize the string streams and demonstrate how programs use them.

C-string1 string Class2
Header Files <strstream> <sstream>
Classes istrstream
ostrstream
istringstream
ostringstream
Possible
Implementation
A UML class diagram illustrating an inheritance relationship between the istream superclass and the istrstream subclass. The istrstream subclass has a character pointer data member storing a C-string. A UML class diagram illustrating the istringstream subclass with two superclasses: string and istream.
String Stream Classes. String streams combine the features of strings (C-strings and string objects) with input and output streams. Following good object-oriented design, programmers can't "see" inside the stream classes, and, therefore, can't know how they are implemented. If they maintain the ANSI-required public interface and behaviors, compilers may implement them as they choose. The illustrated UML classes demonstrate how the string streams may simultaneously reflect the behaviors of strings and streams. Although the diagrams illustrate input streams, output streams follow the same structure, replacing the "i" with "o" in the names.
  1. Like the C-string functions presented in Chapter 8, look for the phrase "str" in the header and class names to identify them as the C-string versions.
  2. The string stream header file name begins with "s" (presumably for brevity), but the class names include the phrase "string," identifying them as the string versions.

 

istrstream	n1("123");		// (a)
istrstream	n2("3.14");
int		i;			// (b)
double		d;

n1 >> i;				// (c)
n2 >> d;
cout << "i = " << i << endl;		// (d)
cout << "d = " << d << endl;
char		a[100];			// (e)
ostrstream	i(a, 100);		// (f)
ostrstream	f(a, 100);

i << 76 << ends;			// (g)
cout << "a = " << a << endl;

f << 76.58 << ends;			// (h)
cout << "a = " << a << endl;
Converting numbers to and from C-strings with streams. The examples are trivial but demonstrate how to convert between C-strings and numbers with the stream classes. Authentic programs replace the constant values with variables.
  1. Defines istrstream objects
  2. Defines numeric variables.
  3. Converts C-strings to numbers
  4. Statements demonstrating the conversion
  1. Defines a character array used as a C-string
  2. Defines ostrstream objects using the C-string array
  3. Converts an integer to a C-string and displays it
  4. Converts a double to a C-string and displays it
These classes are deprecated in favor of the string versions. Nevertheless, programmers still occasionally use C-strings (for example, for processing command line input), making these classes useful in limited cases.

 

string		s1("123");		// (a)
string		s2("3.14");
istringstream	n1(s1);
istringstream	n2(s2);
int		i;
double		d;

n1 >> i;				// (b)
n2 >> d;
cout << "i = " << i << endl;		// (c)
cout << "d = " << d << endl;
ostringstream	i;			// (d)
ostringstream	f;

i << 76;				// (e)
string		s1 = i.str();		// (f)
cout << "s1 = " << s1 << endl;		// (g)

f << 76.58;				// (e)
string		s2 = f.str();		// (f)
cout << "s2 = " << s2 << endl;		// (g)
 
Converting numbers to and from string objects. Although the examples don't solve a "real" problem, they do demonstrate the syntax for converting numbers to and from instances of the string class. String streams can utilize all of the formatting functions and manipulators described previously.
  1. Defines the string, stream, and numeric variables the example uses
  2. Converting string objects to numbers
  3. Statements demonstration the conversions
  1. The stream objects performing the conversion
  2. Writing a numeric value to an ostringstream object
  3. The str() function returns the stream's string member
  4. Prints the converted value
See istringstream and ostringstream for more detail.

Rereading A string: A More Robust Rolodex

The final version of the s-rolodex.cpp program still has some problems. If the input file is empty, or a line in the file is empty or doesn't match the expected pattern, the program can crash or incorrectly parse the data. An easy solution is to read and process the file by lines, skipping those that don't match. However, once read, processing correct lines is more cumbersome than using the getline function to read individual fields. An elegant and more robust solution rereads the fields from matching lines.

while (!in.eof())
{
	string	line;					// (a)
	getline(in, line);

	if (line.length() == 0 || line[0] == '#')	// (b)
		continue;

	istringstream input(line);			// (c)

	string	name;					// (d)
	getline(input, name, ':');

	string	address;
	getline(input, address, ':');

	string	phone;
	getline(input, phone, '\n');

	cout << left << setw(20) << name <<
		setw(35) << address <<
		setw(20) << phone << endl;
}
Bill Gates:1 Microsoft Way, Redmond, WA:(403) 123-4567
Cranston Snort:1600 Pennsylvania Ave:(306) 678-9876

Albert Einstein:Princeton, NJ:(456) 123-8765
# This is a comment
John Smith:123 Elm St.:801-555-1234
Rereading a file with istringstream. The figure excerpts and updates the while loop from the previous s-rolodex program.
  1. Together, the statements read a complete line from the file associated with in, an instance of ifstream.
  2. The if-statement skips empty lines and empty files. Configuration files often use a '#' character in the left-most column to denote comments. Although not a stated problem requirement, the rereading technique makes this an easy addition. The order of the two sub-tests is significant: the expression line[0] is a logical error if the line is empty. Short circuit evaluation solves the problem.
  3. Creates an istringstream object that takes its input from the string line.
  4. The getline functions continue reading the colon-delimited fields but take their input from input rather than in.

Balancing A Checkbook: Number Conversion With istringstream

At one time in industrialized countries, it was a common practice for people to write checks to pay bills and for routine purchases. Checkbooks often included a "register" for people to record the written checks and account deposits. Disciplined people frequently "balanced their checkbook," reconciling their account and maintaining an accurate record of available funds. The checkbook example builds on this (perhaps idealized) situation by simulating a checkbook register and calculating a balance.

Reading and Processing LoopData File
double balance = 0;								// (a)

while (!in.eof())
{
    string    entry;
    getline(in, entry);

    if (entry.length() == 0 || entry[0] == '#')
        continue;

    istringstream input(entry);

    string    type;
    getline(input, type, ':');

    string    date;
    getline(input, date, ':');

    string    to;
    getline(input, to, ':');

    double amount;
    input >> amount;								// (b)

    if (type == "Deposit" || type == "deposit")					// (c)
        balance += amount;
    else
        balance -= amount;

    cout << left << setw(10) << type <<
        setw(10) << date <<
        setw(20) << to << 
        right << setw(10) << fixed << setprecision(2) << amount << endl;
}

cout << right << setw(50) << fixed << setprecision(2) << balance << endl;	// (d)
deposit:July 7:-:300
416:July 8:Gas Company:15.85
417:7/9:Auto Store:19.95
418:7/10:Grocery Store:47.50
419:Dec 5:Hardware Store:47.89
Deposit:8/19/2006:-:150.00
Converting numeric fields with istringstream: A simulated checkbook. The basic structure of the checkbook example parallels the Rolodex program above. Each line in the data file consists of four fields: the check number or the word "Deposit," the data, the check's recipient or a '-' for a deposit, and an amount. The program reads the first three fields from an istringstream object, as illustrated in the previous figure, but it extracts the last field, the amount, as a number.
  1. balance is an accumulator maintaining a running balance as the program processes the data file. The program defines it outside and above the loop so that its value is not reset during each iteration and, it remains in scope to print the total.
  2. Reads the amount from the istringstream object. Just as streams maintain a position in a file, string streams maintain a position in a string. So, the three calls to getline leave the stream's position ready for it to read the amount.
  3. The program determines whether to add or subtract the amount from the balance based on the contents of the first field.
  4. Prints the final checkbook balance. Can you see how the programmer determined the field width of 50?

Reading a complete line from a file and validating it before processing can solve some but not all the stated problems, leaving one unresolved. If the line is not empty and not a comment but still doesn't match the expected field and delimiter pattern, it passes the simple validation test but fails the processing operations. Programmers can craft a solution using the find, rfind, andsubstr functions, but it's awkward, error-prone, and very rigid. Even small changes to the file's pattern can render such a solution unusable. This problem requires an even more robust and general solution.

String Stream Example Files

ViewDownloadComments
s-rolodex2.cpp s-rolodex2.cpp A more robust version of the Rolodex program that safely excludes blank lines (including at the end of the file) and comment lines beginning with the '#' character.
s-rolodex2.txt s-rolodex2.txt An input file with data, blank lines, and comments.
checkbook.cpp checkbook.cpp A program reading a "checkbook register" file, printing it in a tabular format and calculating a balance.
checkbook.txt checkbook.txt A checkbook register file.