EXPLORATION 56

image

Text I/O

Input and output have two basic flavors: text and binary. Binary I/O introduces subtleties that are beyond the scope of this book, so all discussion of I/O herein is text-oriented. This Exploration presents a variety of topics related to textual I/O. You’ve already seen how the input and output operators work with the built-in types as well as with the standard library types, when it makes sense. You’ve also seen how you can write your own I/O operators for custom types. This Exploration offers some additional details about file modes, reading and writing strings, and converting values to and from strings.

File Modes

Exploration 14 briefly introduced the file stream classes ifstream and ofstream. The basic behavior is to take a file name and open it. You gain a little more control than that by passing a second argument, which is a file mode. The default mode for an ifstream is std::ios_base::in, which opens the file for input. The default mode for ofstream is std::ios_base::out | std::ios_base::trunc. (The | operator combines certain values, such as modes. Exploration 64 will cover this in depth.) The out mode opens the file for output. If the file doesn’t exist, it is created. The trunc mode means to truncate the file, so you always start with an empty file. If you explicitly specify the mode and omit trunc, the old contents (if any) remain. Therefore, by default, writing to the output stream overwrites the old contents. If you want to position the stream at the end of the old contents, use the ate mode (short for at-end), which sets the stream’s initial position to the end of the existing file contents. The default is to position the stream at the start of the file.

Another useful mode for output is app (short for append), which causes every write to append to the file. That is, app affects every write, whereas ate affects only the starting position. The app mode is useful when writing to a log file.

Write a debug() functionthat takes a single string as an argument and writes that string to a file named “debug.txt”. Listing 56-1 shows the header that declares the function.

Listing 56-1.  Header That Declares a Trivial Debugging Function

#ifndef DEBUG_HPP_
#define DEBUG_HPP_
 
#include <string>
 
/** @brief Write a debug message to the file @c "debug.txt"
 * @param msg The message to write
 */
void debug(std::string const& msg);
 
#endif

Append every log message to the file, terminating each message with a newline. To ensure that the debugging information is properly recorded, even if the program crashes, open the file anew every time the debug() function is called. Listing 56-2 shows my solution.

Listing 56-2.  Implementing the Debug Function

#include <fstream>
#include <ostream>
#include <stdexcept>
 
#include <string>
#include "debug.hpp"
 
void debug(std::string const& str)
{
   std::ofstream stream{"debug.txt", std::ios_base::out | std::ios_base::app};
   if (not stream)
      throw std::runtime_error("cannot open debug.txt");
   stream.exceptions(std::ios_base::failbit);
   stream << str << ' ';
   stream.close();
}

String Streams

In addition to file streams, C++ offers string streams. The <sstream> header defines istringstream and ostringstream. A string stream reads from and writes to a std::string object. For input, supply the string as an argument to the istringstream constructor. For output, you can supply a string object, but the more common usage is to let the stream create and manage the string for you. The stream appends to the string, allowing the string to grow as needed. After you are finished writing to the stream, call the str() member function to retrieve the final string.

Suppose you have to read pairs of numbers from a file representing a car’s odometer reading and the amount of fuel needed to fill the tank. The program computes the miles per gallon (or liters per kilometer, if you prefer) at each fill-up and overall. The file format is simple: each line has the odometer reading, followed by the fuel amount, on one line, separated by white space.

Write the program. Listing 56-3 demonstrates the miles-per-gallon approach.

Listing 56-3.  Computing Miles per Gallon

#include <iostream>
 
int main()
{
   double total_fuel{0.0};
   double total_distance{0.0};
   double prev_odometer{0.0};
   double fuel{}, odometer{};
   while (std::cin >> odometer >> fuel)
   {
      if (fuel != 0)
      {
         double distance{odometer - prev_odometer};
         std::cout << distance / fuel << ' ';
         total_fuel += fuel;
         total_distance += distance;
         prev_odometer = odometer;
      }
   }
   if (total_fuel != 0)
      std::cout << "Net MPG=" << total_distance / total_fuel << ' ';
}

Listing 56-4 shows the equivalent program, but computing liters per kilometer. For the remainder of this Exploration, I will use miles per gallon. Readers who don’t use this method can consult the files that accompany the book for liters per kilometer.

Listing 56-4.  Computing Liters per Kilometer

#include <iostream>
 
int main()
{
   double total_fuel{0.0};
   double total_distance{0.0};
   double prev_odometer{0.0};
   double fuel{}, odometer{};
   while (std::cin >> odometer >> fuel)
   {
      double distance{odometer - prev_odometer};
      if (distance != 0)
      {
         std::cout << fuel / distance << ' ';
         total_fuel += fuel;
         total_distance += distance;
         prev_odometer = odometer;
      }
   }
   if (total_distance != 0)
      std::cout << "Net LPK=" << total_fuel / total_distance << ' ';
}

What happens if the user accidentally forgets to record the fuel on one line of the file?

_____________________________________________________________

The input loop doesn’t know or care about lines. It resolutely skips over white space in its quest to fulfill each input request. Thus, it reads the subsequent line’s odometer reading as a fuel amount. Naturally, the results will be incorrect.

A better solution would be to read each line as a string and extract two numbers from the string. If the string is not formatted correctly, issue an error message and ignore that line. You read a line of text into a std::string by calling the std::getline function (declared in <string>). This function takes an input stream as the first argument and a string object as the second argument. It returns the stream, which means it returns a true value if the read succeeds or a false one if the read fails, so you can use the call to getline as a loop condition.

Once you have the string, open an istringstream to read from the string. Use the string stream the same way you would any other input stream. Read two numbers from the string stream. If the string stream does not contain any numbers, ignore that line. If it contains only one number, issue a suitable error message. Listing 56-5 presents the new program.

Listing 56-5.  Rewriting the Miles-per-Gallon Program to Parse a String Stream

#include <iostream>
#include <sstream>
#include <string>
 
int main()
{
   double prev_odometer{0.0};
   double total_fuel{0.0};
   double total_distance{0.0};
   std::string line{};
   int linenum{0};
   bool error{false};
   while (std::getline(std::cin, line))
   {
      ++linenum;
      std::istringstream input{line};
      double odometer{};
      if (input >> odometer)
      {
         double fuel{};
         if (not (input >> fuel))
         {
            std::cerr << "Missing fuel consumption on line " << linenum << ' ';
            error = true;
         }
         else if (fuel != 0)
         {
            double distance{odometer - prev_odometer};
            std::cout << distance / fuel << ' ';
            total_fuel += fuel;
            total_distance += distance;
            prev_odometer = odometer;
         }
      }
   }
   if (total_fuel != 0)
   {
      std::cout << "Net MPG=" << total_distance / total_fuel;
      if (error)
         std::cout << " (estimated, due to input error)";
      std::cout << ' ';
   }
}

Most text file formats allow some form of annotation or commentary. The file format already allows one form of commentary, as a side effect of the program’s implementation. How can you add comments to the input file?

_____________________________________________________________

After the program reads the fuel amount from a line, it ignores the rest of the string. You can add comments to any line that contains the proper odometer and fuel data. But that’s a sloppy side effect. A better design requires the user to insert an explicit comment marker. Otherwise, the program might misinterpret erroneous input as a valid input, followed by a comment, such as accidentally inserting an extra space, as illustrated in the following:

123  21 10.23

Let’s modify the file format. Any line that begins with a pound sign (#) is a comment. Upon reading a comment character, the program skips the entire line. Add this feature to the program. A useful function is an input stream’s unget() function. After reading a character from the stream, unget() returns that character to the stream, causing the subsequent read operation to read that character again. In other words, after reading a line, read a character from the line, and if it is '#', skip the line. Otherwise, call unget() and continue as before. Compare your result with mine, as shown in Listing 56-6.

Listing 56-6.  Parsing Comments in the Miles-per-Gallon Data File

#include <iostream>
#include <sstream>
#include <string>
 
int main()
{
   double total_fuel{0.0};
   double total_distance{0.0};
   double prev_odometer{0.0};
   std::string line{};
   int linenum{0};
   bool error{false};
   while (std::getline(std::cin, line))
   {
      ++linenum;
      std::istringstream input{line};
      char comment{};
      if (input >> comment and comment != '#')
      {
         input.unget();
         double odometer{};
         if (input >> odometer)
         {
           double fuel{};
            if (not (input >> fuel))
            {
               std::cerr << "Missing fuel consumption on line " << linenum << ' ';
               error = true;
            }
            else if (fuel != 0)
            {
               double distance{odometer - prev_odometer};
               std::cout << distance / fuel << ' ';
               total_fuel += fuel;
               total_distance += distance;
               prev_odometer = odometer;
            }
         }
      }
   }
   if (total_fuel != 0)
   {
      std::cout << "Net MPG=" << total_distance / total_fuel;
      if (error)
         std::cout << " (estimated, due to input error)";
      std::cout << ' ';
   }
}

More complicated still is allowing the comment marker to appear anywhere on a line. A comment extends from the # character to the end of the line. The comment marker can appear anywhere on a line, but if the line contains any data, it must contain two valid numbers prior to the comment marker. Enhance the program to allow comment markers anywhere. Consider using the find() member function of std::string. It has many forms, one of which takes a character as an argument and returns the zero-based index of the first occurrence of that character in the string. The return type is std::string::size_type. If the character is not in the string, find() returns the magic constant std::string::npos.

Once you find the comment marker, you can delete the comment by calling erase() or copy the non-comment portion of the string by calling substr(). String member functions work with zero-based indices. Substrings are expressed as a starting position and a count of the number of characters affected. Usually, the count can be omitted to mean the rest of the string. Compare your solution with mine, presented in Listing 56-7.

Listing 56-7.  Allowing Comments Anywhere in the Miles-per-Gallon Data File

#include <iostream>
#include <sstream>
#include <string>
 
int main()
{
   double total_fuel{0.0};
   double total_distance{0.0};
   double prev_odometer{0.0};
   std::string line{};
   int linenum{0};
   bool error{false};
   while (std::getline(std::cin, line))
   {
      ++linenum;
      std::string::size_type comment{line.find('#')};
      if (comment != std::string::npos)
         line.erase(comment);
      std::istringstream input{line};
      double odometer{};
      if (input >> odometer)
      {
         double fuel{};
         if (not (input >> fuel))
         {
            std::cerr << "Missing fuel consumption on line " << linenum << ' ';
            error = true;
         }
         else if (fuel != 0)
         {
            double distance{odometer - prev_odometer};
            std::cout << distance / fuel << ' ';
            total_fuel += fuel;
            total_distance += distance;
            prev_odometer = odometer;
         }
      }
   }
   if (total_fuel != 0)
   {
      std::cout << "Net MPG=" << total_distance / total_fuel;
      if (error)
         std::cout << " (estimated, due to input error)";
      std::cout << ' ';
   }
}

Now that the file format allows explicit comments on each line, you should add some more error-checking to make sure that each line contains only two numbers, and nothing more (after removing comments). One way to check is to read a single character after reading the two numbers. If the read succeeds, the line contains erroneous text. Add error-checking to detect lines with extra text. Compare your solution with my solution, shown in Listing 56-8.

Listing 56-8.  Adding Error-Checking for Each Line of Input

#include <iostream>
#include <sstream>
#include <string>
 
int main()
{
   double total_fuel{0.0};
   double total_distance{0.0};
   double prev_odometer{0.0};
   std::string line{};
   int linenum{0};
   bool error{false};
   while (std::getline(std::cin, line))
   {
      ++linenum;
      std::string::size_type comment{line.find('#')};
      if (comment != std::string::npos)
         line.erase(comment);
      std::istringstream input{line};
      double odometer{};
      if (input >> odometer)
      {
         double fuel{};
         char check{};
         if (not (input >> fuel))
         {
            std::cerr << "Missing fuel consumption on line " << linenum << ' ';
            error = true;
         }
         else if (input >> check)
         {
            std::cerr << "Extra text on line " << linenum << ' ';
            error = true;
         }
         else if (fuel != 0)
         {
            double distance{odometer - prev_odometer};
            std::cout << distance / fuel << ' ';
            total_fuel += fuel;
            total_distance += distance;
            prev_odometer = odometer;
         }
      }
   }
   if (total_fuel != 0)
   {
      std::cout << "Net MPG=" << total_distance / total_fuel;
      if (error)
         std::cout << " (estimated, due to input error)";
      std::cout << ' ';
   }
}

Text Conversion

Let me put on my clairvoyance cap for a moment . . . I can see that you have many unanswered questions about C++; and one of those questions is, “How can I convert a number to a string easily, and vice versa?”

The standard library offers some simple functions: std::to_string() takes an integer and returns a string representation. To convert a string to an integer, choose from several functions, depending on the desired return type: std::stoi() returns an int, and std::stod() returns double.

But these functions offer little flexibility. You know that I/O streams offer lots of flexibility and control over formatting. Surely, you say, you can create functions that are just as easy to use with suitable defaults, but also offer some flexibility in formatting (such as floating-point precision, fill characters, etc.).

Now that you know how to use string streams, the way forward is clear: use an istringstream to read a number from a string, or use an ostringstreamto write a number to a string. The only task is to wrap up the functionality in an appropriate function. Even better is to use a template. After all, reading or writing an int is essentially the same as reading or writing a long, etc.

Listing 56-9 shows the from_string function template, which has a single template parameter, T—the type of object to convert. The function returns type T and takes a single function argument: a string to convert.

Listing 56-9.  The from_string Function Extracts a Value from a String

#include <istream> // for the >> operator
#include <sstream> // for istringstream
#include <string>  // for string
#include "conversion_error.hpp"
 
template<class T>
T from_string(std::string const& str)
{
  std::istringstream in{str};
  T result{};
  if (in >> result)
    return result;
  else
    throw conversion_error{str};
}

The conversion_error class is a custom exception class. The details are not relevant to this discussion, but inquisitive readers can satisfy their curiosity with the files that accompany this book. T can be any type that permits reading from an input stream with the >> operator, including any custom operators and types that you write.

Now what about the advertised flexibility? Let’s add some. As written, the from_string function does not check for text that follows the value. Plus, it skips over leading white space. Modify the function to take a bool argument:skipws. If true, from_string skips leading white space and allows trailing white space. If false, it does not skip leading white space, and it does not permit trailing white space. In both cases, it throws conversion_error, if invalid text follows the value. Compare your solution with mine in Listing 56-10.

Listing 56-10.  Enhancing the from_string Function

#include <ios>     // for std::noskipws
#include <istream> // for the >> operator
#include <sstream> // for istringstream
#include <string>  // for string
#include "conversion_error.hpp"
 
template<class T>
T from_string(std::string const& str, bool skipws = true)
{
  std::istringstream in{str};
  if (not skipws)
    in >> std::noskipws;
  T result{};
  char extra;
  if (not (in >> result))
    throw conversion_error{str};
  else if (in >> extra)
    throw conversion_error{str};
  else
    return result;
}

I threw in a new language feature. The function parameter skipws is followed by = true, which looks like an assignment or initialization. It lets you call from_string with one argument, just as before, using true as the second argument. This is how file streams specify a default file mode, in case you were wondering. If you decide to declare default argument values, you must supply them starting with the right-most argument in the argument list. I don’t use default arguments often, and in Exploration 69, you will learn about some subtleties related to default arguments and overloading. For now, use them when they help, but use them sparingly.

Your turn to write a function from scratch. Write the to_string function template, which takes a single template argument and declares the to_string function to take a single function argument of that type. The function converts its argument to a string by writing it to a string stream and returns the resulting string. Compare your solution with mine, presented in Listing 56-11.

Listing 56-11.  The to_string Function Converts a Value to a String

#include <ostream> // for the << operator
#include <sstream> // for ostringstream
#include <string>  // for string
 
template<class T>
std::string to_string(T const& obj)
{
  std::ostringstream out{};
  out << obj;
  return out.str();
}

Can you see any particular drawback to these functions? ________________ If so, what?

_____________________________________________________________

No doubt, you can see many problems, but in particular, the one I want to point out is that they don’t work with wide characters. It would be a shame to waste all that effort you spent in understanding wide characters, so let’s add another template parameter for the character type. Remember that the std::string class template has three template parameters: the character type, the character traits, and an allocator object to manage any heap memory that the string might use. You don’t have to know any of the details of these three types; you need only pass them to the basic_string class template. The basic_ostringstream class template takes the first two template arguments.

Your first attempt at implementing to_string may look a little bit like Listing 56-12.

Listing 56-12.  Rewriting to_string As a Template Function

#include <ostream> // for the << operator
#include <sstream> // for ostringstream
#include <string>  // for basic_string
 
template<class T, class Char, class Traits, class Allocator>
std::basic_string<Char, Traits, Allocator> to_string(T const& obj)
{
  std::basic_ostringstream<Char, Traits> out{};
  out << obj;
  return out.str();
}

This implementation works. It’s correct, but it’s clumsy. Try it. Try to write a simple test program that converts an integer to a narrow string and the same integer to a wide string. Don’t be discouraged if you can’t figure out how to do it. This exercise is a demonstration of how templates in the standard library can lead you astray if you aren’t careful. Take a look at my solution in Listing 56-13.

Listing 56-13.  Demonstrating the Use of to_string

#include <iostream>
#include "to_string.hpp"
#include "from_string.hpp"
 
int main()
{
    std::string str{
      to_string<int, char, std::char_traits<char>, std::allocator<char>>(42)
    };
    int value{from_string<int>(str)};
}

Do you see what I mean? How were you supposed to know what to provide as the third and fourth template arguments? Don’t worry, we can find a better solution.

One alternative approach is not to return the string, but to take it as an output function argument. The compiler could then use argument-type deduction, and you wouldn’t have to specify all those template arguments. Write a version of to_string that takes the same template parameters but also two function arguments: the value to convert and the destination string. Write a demonstration program to show how much simpler this function is to use. Listing 56-14 shows my solution.

Listing 56-14.  Passing the Destination String As an Argument to to_string

#include <ostream> // for the << operator
#include <sstream> // for ostringstream
#include <string>  // for string
#include "from_string.hpp"
 
template<class T, class Char, class Traits, class Allocator>
void to_string(T const& obj, std::basic_string<Char, Traits, Allocator>& result)
{
  std::basic_ostringstream<Char, Traits> out{};
  out << obj;
  result = out.str();
}
 
int main()
{
    std::string str{};
    to_string(42, str);
    int value(from_string<int>(str));
}

On the other hand, if you want to use the string in an expression, you still have to declare a temporary variable just to hold the string.

Another way to approach this problem is to specify std::string or std::wstring as the sole template argument. The compiler can deduce the type of the object you want to convert. The basic_string template has member typedefs for its template parameters, so you can use those to discover the traits and allocator types. The to_string function returns the string type and takes an argument of the object type. Both types have to be template parameters. Which parameter should be first? Listing 56-15 shows the latest incarnation of to_string, which now takes two template parameters: the string type and the object type.

Listing 56-15.  Improving the Calling Interface of to_string

#include <ostream> // for the << operator
#include <sstream> // for ostringstream
 
template<class String, class T>
String to_string(T const& obj)
{
  std::basic_ostringstream<typename String::value_type,
                           typename String::traits_type> out{};
  out << obj;
  return out.str();
}

Remember typename from Exploration 53? The compiler doesn’t know that String::value_type names a type. A specialization of basic_ostringstream could declare it to be anything. The typename keyword tells the compiler that you know the name is for a type. Calling this form of to_string is straightforward.

to_string<std::string>(42);

This form seems to strike the best balance between flexibility and ease-of-use. But can we add some more formatting flexibility? Should we add width and fill characters? Field adjustment? Hexadecimal or octal? What if to_string takes std::ios_base::fmtflags as an argument, and the caller can specify any formatting flags? What should be the default? Listing 56-16 shows what happens when the author goes overboard.

Listing 56-16.  Making to_string Too Complicated

#include <ios>
#include <ostream> // for the << operator
#include <sstream> // for ostringstream
 
template<class String, class T>
String to_string(T const& obj,
  std::ios_base::fmtflags flags = std::ios_base::fmtflags{},
  int width = 0,
  char fill = ' ')
{
  std::basic_ostringstream<typename String::value_type,
                           typename String::traits_type> out{};
  out.flags(flags);
  out.width(width);
  out.fill(fill);
  out << obj;
  return out.str();
}

Listing 56-17 shows some examples of calling this form of to_string.

Listing 56-17.  Calling to_string

#include <iostream>
#include "to_string.hpp"
 
int main()
{
  std::cout << to_string<std::string>(42, std::ios_base::hex) << ' ';
  std::cout << to_string<std::string>(42.0, std::ios_base::scientific, 10) << ' ';
  std::cout << to_string<std::string>(true, std::ios_base::boolalpha) << ' ';
}

You should see the following as output:

2a
 
4.200000e+01
true

That concludes Part 3. Time for a project.

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

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