Writing Custom I/O

Some programmers aren’t big fans of overloading the standard operators for user-defined C++ classes. This includes writing custom inserters and extractors. However, many programmers prefer creating their own custom input/output operators. This introduction takes you through the steps of creating first a custom inserter (output) and then a custom extractor (input).

Create the class

Create your class without regards to the input or output. Include get and set functions for retrieving and setting individual values within an object. The following example is a Student class for use in this section. Here are the contents of the Student.h include file:

This code doesn’t include the implementation of the get and set methods. Their details should not influence the creation of the inserter and extractor.

The assumption above is that the set functions perform some type of checks to detect invalid input — for example, setGPA() should not allow you to set the grade point average to a value outside of the range 0.0 to 4.0.

Create a simple inserter

The inserter should output a Student object either for display or to a file. The following prototype would be added to the Student.h include file:

ostream& operator<<(ostream& out, const Student& s);

The inserter is declared with a return type of ostream& and returns the object passed to it. Otherwise, a command like the following would not work:

cout << "My student is " << myStudent << endl;

The object returned from << myStudent is used in the << endl that follows.

The implementation of this inserter is straightforward (normally this would appear in the Student.cpp file):

If this is okay, then you’re done. However, for professional applications, you probably want to implement a few output rules like the following (these are just examples):

*A school at which student IDs are six digits in length. If the number is less than a full six digits, then the number should be padded on the left with zeros.

*Grade point averages are normally displayed with two digits after the decimal point.

Fortunately there are controls that implement these features. However, be a little careful before adjusting output formatting. For example, suppose the inserter you wanted to output an integer parameter is in hexadecimal format. Users of the inserter would be quite surprised if all subsequent output appeared in hex rather than decimal. Therefore it’s important to record what the previous settings are and restore them before returning from our inserter.

You can see that the inserter outputs the student’s name just as before. However, before outputting the student’s ID, it sets the field width to six characters and sets the left fill character to 0.

The field width applies to only the very next output so it’s important to set this value immediately before the field you want to impact. Because it lasts for only one field, it is not necessary to record and restore the field width.

Once the field has been output, the fill character is restored to whatever it was before. Similarly the precision is set to three digits and the decimal point is forced on before displaying the grade point average. This forces a 3 to display as 3.00.

The resulting output appears as follows:

Davis (123456)/3.50
Eddins (000001)/3.00

The extractor

The job of creating the extractor actually starts with the inserter. Notice that in creating the output format for my Student object, the programmer added certain special markings that would allow an extractor to make sure that what it’s reading is actually a Student.For example, she included parentheses around the student ID and a slash between it and the GPA.

My extractor can read these fields to make sure that it’s staying in sync with the data stream. In overview, the extractor will need to accomplish the following tasks:

*Read the name (a character string).

*Read an open parenthesis.

*Read an ID (an integer).

*Read a closed parenthesis.

*Read a slash.

*Read the GPA (a floating point number).

It will need to do this while all the time being aware of whether a formatting problem occurs. What follows is my version of the Student extractor:

This inserter starts by reading each of the expected fields as previously outlined in the flow chart. If any of the marker characters is missing (the open parenthesis, closed parenthesis, and slash), then this is not a legal Student object. The approved way to indicate such a failure is set the failbit in the input object. This will stop all further input until the failure is cleared.

The next step is to actually attempt to store these values into the Student. For example, all of the markers may have been present, but the value read for the grade point average may have been 8.0, a value that is clearly out of our predefined range of 0 through 4.0. Assuming that the Student object includes checks for out-of-range values in its set methods, the call to setGPA() will throw an exception which is caught in the extractor. Again, the extractor sets the failbit. Whether the extractor rethrows the exception is up to the programmer.

It’s much better to rely on the setGPA() method to detect the range problem than to implement such a check in the extractor itself. Better to let the Student object protect its own state than to rely on external functions.

Use these new methods

The following (very) simple CustomIO program shows how these inserter and extractor methods are used. This example along with the custom Student inserter and extractor are available at Dummies.com:

Notice that the Davis student displays just as we wanted: the student id is surrounding by parentheses and the grade point average appears with two digits after the decimal point. This latter is true even for the somewhat problematic Eddins student.

The Laskowski student is accepted as input because it has all of the proper marks of a valid student: parentheses and slashes in all the right places. Check out what happens if we leave something off, however:

Input a student object:Laskowski 234567/3.75
Error reading student

This small program shows two very important habits that you should develop:

Check the fail flag by calling fail() after every input.

Clear the fail flag as soon as you’ve acknowledged the failure by calling clear().

ALL subsequent requests for input will be ignored until you’ve cleared the fail flag.