Online Test Banks
Score higher
See Online Test Banks
eLearning
Learning anything is easy
Browse Online Courses
Mobile Apps
Learning on the go
Explore Mobile Apps
Dummies Store
Shop for books and more
Start Shopping

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:

class Student
{
  public:
    explicit Student(const char* pszName = "",
                     long  id = 0,
                     double gpa = 0.0);
    // the get and set functions for this class
    string getName() const;
    void setName(string& s);
    long getID() const;
    void setID(long nID);
    double getGPA() const;
    void setGPA(double dGPA);
};

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):

ostream& operator<<(ostream& out, const Student& s)
{
    int prev = out.precision(2);
    out << s.getName()
        << " (" << s.getID() << ")"
        << "/" << s.getGPA();
    return out;
}

Try it out and make adjustments

So let's see how this inserter works:

// CustomIO - develop a custom inserter and extractor
#include <cstdlib>
#include <cstdio>
#include <iostream>
#include "student.h"
using namespace std;
int main(int argc, char* pArgs[])
{
    Student s1("Davis", 123456, 3.5);
    cout << s1 << endl;
    Student s2("Eddins", 1, 3);
    cout << s2 << endl;
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, '\n');
    cin.get();
    return 0;
}

This generates the following output:

Davis (123456) /3.5
Eddins (1) /3

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.

One version of the gussied up inserter appears as follows:

ostream& operator<<(ostream& out, const Student& s)
{
    out << s.getName()
        << " (";
    // force the id to be a six digit field
    char prevFill = out.fill('0');
    out.width(6);
    out << s.getID();
    out.fill(prevFill);
    // now output the rest of the Student
    int prevPrec = out.precision(3);
    ios_base::fmtflags prev=out.setf(ios_base::showpoint);
    out << ")" << "/" << s.getGPA();
    out.precision(prevPrec);
    out.setf(prev);
    return out;
}

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:

istream& operator>>(istream& in, Student& s)
{
    // read values (ignore extra white space)
    string name;
    long nID;
    double dGPA;
    char openParen = 0, closedParen = 0, slash = 0;
    ios_base::fmtflags prev = in.setf(ios_base::skipws);
    in >> name
       >> openParen >> nID >> closedParen
       >> slash >> dGPA;
    in.setf(prev);
    // if the markers don't match...
    if (openParen!='(' || closedParen!=')' || slash!='/')
    {
        // ...then this isn't a legal Student
        in.setstate(ios_base::failbit);
        return in;
    }
    // try to set the student values
    try
    {
        s.setName(name);
        s.setID(nID);
        s.setGPA(dGPA);
    }
    catch (...)
    {
        // something's not right - flag the failure
        in.setstate(ios_base::failbit);
        throw;
    }
    return in;
}

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:

int main(int argc, char* pArgs[])
{
    Student s1("Davis", 123456, 3.5);
    cout << s1 << endl;
    Student s2("Eddins", 1, 3);
    cout << s2 << endl;
    cout << "Input a student object:";
    cin >> s1;
    if (cin.fail())
    {
        cout << "Error reading student " << endl;
        cin.clear();
    }
    else
    {
        cout << "Read: " << s1 << endl;
    }
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, '\n');
    cin.get();
    return 0;
}

The output from this program (when provided a legal student as input appears as follows:

Davis (123456)/3.50
Eddins (000001)/3.00
Input a student object:Laskowski (234567)/3.75
Read: Laskowski (234567)/3.75
Press Enter to continue...

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.

  • Add a Comment
  • Print
  • Share
blog comments powered by Disqus
Advertisement

Inside Dummies.com

Dummies.com Sweepstakes

Win $500. Easy.