EXPLORATION 31

image

Custom I/O Operators

Wouldn’t it be nice to be able to read and write rational numbers directly, for example, std::cout << rational{355, 113}? In fact, C++ has everything you need, although the job is a little trickier than perhaps it should be. This Exploration introduces some of the pieces you need to accomplish this.

Input Operator

The I/O operators are just like any other operators in C++, and you can overload them the way you overload any other operator. The input operator, also known as an extractor (because it extracts data from a stream), takes std::istream& as its first parameter. It must be a non-const reference, because the function will modify the stream object. The second parameter must also be a non-const reference, because you will store the input value there. By convention, the return type is std::istream&, and the return value is the first parameter. That lets you combine multiple input operations in a single expression. (Go back to Listing 17-3 for an example.)

The body of the function must do the work of reading the input stream, parsing the input, and deciding how to interpret that input. Proper error handling is difficult, but the basics are easy. Every stream has a state mask that keeps track of errors. Table 31-1 lists the available state flags (declared in <ios>).

Table 31-1. I/O State Flags

Flag

Description

badbit

Unrecoverable error

eofbit

End of file

failbit

Invalid input or output

goodbit

No errors

If the input is not valid, the input function sets failbit in the stream’s error state. When the caller tests whether the stream is okay, it tests the error state. If failbit is set, the check fails. (The test also fails if an unrecoverable error occurs, such as a hardware malfunction, but that’s not pertinent to the current topic.)

Now you have to decide on a format for rational numbers. The format should be one that is flexible enough for a human to read and write easily but simple enough for a function to read and parse quickly. The input format must be able to read the output format and might be able to read other formats too.

Let’s define the format as an integer, a slash (/), and another integer. White space can appear before or after any of these elements, unless the white space flag is disabled in the input stream. If the input contains an integer that is not followed by a slash, the integer becomes the resulting value (that is, the implicit denominator is 1). The input operator has to “unread” the character, which may be important to the rest of the program. The unget() member function does exactly that. The input operator for integers will do the same thing: read as many characters as possible until reading a character that is not part of the integer, then unget that last character.

Putting all these pieces together requires a little care, but is not all that difficult. Listing 31-1 presents the input operator. Add this operator to the rest of the rational type that you wrote in Exploration 30.

Listing 31-1.  Input Operator

#include <ios>      // declares failbit, etc.
#include <istream>  // declares std::istream and the necessary >> operators
 
std::istream& operator>>(std::istream& in, rational& rat)
{
  int n{0}, d{0};
  char sep{''};
  if (not (in >> n >> sep))
    // Error reading the numerator or the separator character.
    in.setstate(std::cin.failbit);
  else if (sep != '/')
  {
    // Read numerator successfully, but it is not followed by /.
    // Push sep back into the input stream, so the next input operation
    // will read it.
    in.unget();
    rat.assign(n, 1);
  }
  else if (in >> d)
    // Successfully read numerator, separator, and denominator.
    rat.assign(n, d);
  else
    // Error reading denominator.
    in.setstate(std::cin.failbit);
 
  return in;
}

Notice how rat is not modified until the function has successfully read both the numerator and the denominator from the stream. The goal is to ensure that if the stream enters an error state, the function does not alter rat.

The input stream automatically handles white space. By default, the input stream skips leading white space in each input operation, which means the rational input operator skips white space before the numerator, the slash separator, and the denominator. If the program turns off the ws flag, the input stream does not skip white space, and all three parts must be contiguous.

Output Operator

Writing the output operator, or inserter (so named because it inserts text into the output stream), has a number of hurdles, due to the plethora of formatting flags. You want to heed the desired field width and alignment, and you have to insert fill characters, as needed. Like any other output operator, you want to reset the field width but not change any other format settings.

One way to write a complicated output operator is to use a temporary output stream that stores its text in a string. The std::ostringstream type is declared in the <sstream> header. Use ostringstream the way you would use any other output stream, such as cout. When you are done, the str() member function returns the finished string.

To write the output operator for a rational number, create an ostringstream, and then write the numerator, separator, and denominator. Next, write the resulting string to the actual output stream. Let the stream itself handle the width, alignment, and fill issues when it writes the string. If you had written the numerator, slash, and denominator directly to the output stream, the width would apply only to the numerator, and the alignment would be wrong. Similar to an input operator, the first parameter has type std::ostream&, which is also the return type. The return value is the first parameter. The second parameter can use call-by-value, or you can pass a reference to const, as you can see in Listing 31-2. Add this code to Listing 31-1 and the rest of the rational type that you are defining.

Listing 31-2.  Output Operator

#include <ostream>  // declares the necessary << operators
#include <sstream>  // declares the std::ostringstream type
 
std::ostream& operator<<(std::ostream& out, rational const& rat)
{
  std::ostringstream tmp{};
  tmp << rat.numerator;
  if (rat.denominator != 1)
    tmp << '/' << rat.denominator;
  out << tmp.str();
 
  return out;
}

Error State

The next step is to write a test program. Ideally, the test program should be able to continue when it encounters an invalid-input error. So now is a good time to take a closer look at how an I/O stream keeps track of errors.

As you learned earlier in this Exploration, every stream has a mask of error flags (see Table 31-1). You can test these flags, set them, or clear them. Testing the flags is a little unusual, however, so pay attention.

The way most programs in this book test for error conditions on a stream is to use the stream itself or an input operation as a condition. As you learned, an input operator function returns the stream, so these two approaches are equivalent. A stream converts to a bool result by returning the inverse of its fail() function, which returns true, if failbit or badbit are set.

In the normal course of an input loop, the program progresses until the input stream is exhausted. The stream sets eofbit when it reaches the end of the input stream. The stream’s state is still good, in that fail() returns false, so the loop continues. However, the next time you try to read from the stream, it sees that no more input is available, sets failbit, and returns an error condition. The loop condition is false, and the loop exits.

The loop might also exit if the stream contains invalid input, such as non-numeric characters for integer input, or the loop can exit if there is a hardware error on the input stream (such as a disk failure). Until now, the programs in this book didn’t bother to test why the loop exited. To write a good test program, however, you have to know the cause.

First, you can test for a hardware or similar error by calling the bad() member function, which returns true if badbit is set. That means something terrible happened to the file, and the program can’t do anything to fix the problem.

Next, test for normal end-of-file by calling the eof() member function, which is true only when eofbit is set. If bad() and eof() are both false and fail() is true, this means the stream contains invalid input. How your program should handle an input failure depends on your particular circumstances. Some programs must exit immediately; others may try to continue. For example, your test program can reset the error state by calling the clear() member function, then continue running. After an input failure, you may not know the stream’s position, so you don’t know what the stream is prepared to read next. This simple test program skips to the next line.

Listing 31-3 demonstrates a test program that loops until end-of-file or an unrecoverable error occurs. If the problem is merely invalid input, the error state is cleared, and the loop continues.

Listing 31-3.  Testing the I/O Operators

... omitted for brevity ...
 
/// Tests for failbit only
bool iofailure(std::istream& in)
{
  return in.fail() and not in.bad();
}
 
int main()
{
  rational r{0};
 
  while (std::cin)
  {
    if (std::cin >> r)
      // Read succeeded, so no failure state is set in the stream.
      std::cout << r << ' ';
    else if (iofailure(std::cin))
    {
      // Only failbit is set, meaning invalid input. Clear the state,
      // and then skip the rest of the input line.
      std::cin.clear();
      std::cin.ignore(std::numeric_limits<int>::max(), ' '),
    }
  }
 
  if (std::cin.bad())
    std::cerr << "Unrecoverable input failure ";
}

The rational type is nearly finished. The next Exploration tackles assignment operators and seeks to improve the constructors.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset