Examples

Example: Binary to ASCII Conversion

Output String Streams

The insertion operators (<<) for streams are handy because they translate binary data into sequences of characters. Too bad there are no translators for strings. But like files and the monitor, strings can be sinks for a class of streams called output string streams (ostringstream). An output string stream is a special type of output stream (ostream), so any operations that can be performed on an output stream can also be performed on an output string stream. Thus, any type that defines a variant of the insertion operator can use it to change itself into a string:

template <typename Data>
string toString(const Data& val, bool terminate = false)
{
   ostringstream os;
   os << val;
   if (terminate) os << ends;
   return os.str();
}

Here is a patch of code that uses this template:

string s1 = "the answer is";
string s2 = toString(6 * 7);
string s3 = s1 + " = " + s2; // string concatonation
cout << s3 << '\n';

Input String Streams

Strings can also be character sources for input string streams. An input string stream (istingstream) is a special kind of input stream (istream), so all operations that can be performed on an input stream can also be performed on an input string stream. Here's the mate of the toString() function. This one translates strings into values. We must pass the value as a reference parameter so C++ can infer Value from calls to the function:

template <typename Value>
void fromString(Value& val, const string& str)
{
   istringstream is(str);
   is >> val; // hope >> is defined for Value
}

Here's a sample call::

int x;
fromString(x, "42"); // x = 42

The string stream declarations are in a header file called <sstream>, so you'll need the following include directive:

#include <sstream>

Example: Generic Error Handling

We are constantly reminded that our functions should validate their inputs. But what should a function do if its inputs aren't valid? One solution is to stop the program if the input is invalid. This is fine when we are debugging the program. If we're not, then the solution is to throw an exception that can be handled in a part of the program that can deal with the problem. (More on this later.) We use a #define directive to create a debug mode flag (we could have used a constant declaration, too):

#define DEBUG_MODE true

Our error handling function prints an error message to cerr and calls the library function exit(1). Calling exit(1) is the same as calling return 1 in main(). It immediately terminates the program returning an exit code of 1 (meaning unsuccessful completion) to the operating system. If debug mode is false, an object called an exception is created by calling runtime_error(). The exception encapsulates the error message. The exception object is then thrown. A thrown exception can be caught and handled in another part of the program able to deal with the problem. Uncaught exceptions cause the program to terminate. More on this later. Here's the code:

void error(const string& gripe = "unknown")
{
   if (DEBUG_MODE)
   {
      cerr << "Error: " << gripe << '\n';
      exit(1);
   }
   else
      throw runtime_error(gripe);
}

Example: util.h

Every C++ programmer has files of useful functions that he or she uses in almost every project. Many of the examples in the book include a file called util.h, which is listed in this appendix.

Coding Style Guidelines

util.h follows a simple coding style with only a few basic rules:

1. Names should be chosen to reflect their interpretation. Type names begin with uppercase letters, function, parameter, and variable names begin with lower case letters, and macro names only use uppercase letters.

2. Format should reflect program structure. Indentation levels should reflect nesting levels, blank lines should separate logical tasks, and opening and closing curly braces should be aligned.

3. Comments shouldn't be overdone or underdone. Comments should be literate. Each file begins with a comment that at least contains the author's name. Each function begins with a comment that tells the function's purpose. Non-obvious entry, exit, and exception conditions should be mentioned. Remarks about algorithm can be included if they're helpful.

Many universities and companies have their own style guidelines, which readers are welcome to use in place of mine, but some style guideline must be followed. There are several proposals for standard C++ style guidelines, which readers may also use.

File Structure

Here's the structure of util.h:

/*
 * File:            util.h
 * Programmer:      Pearce
 * Copyright (c):   2000, all rights reserved.
 */
#ifndef UTIL_H
#define UTIL_H
// MS specific:
#pragma warning( disable : 4786)
// includes from C++ lib
#include <string>
#include <vector>
#include <iostream>
#include <sstream>
using namespace std;
// includes from C lib
#include <cmath>
#include <cctype>
#include <cstring>
#include <cerrno>
// templates, macros, and inline utilities go here
#endif

Of course each definition will start with a comment containing a short description followed by any entry or exit conditions.

/*
 * Generic error handling for small programs.
 * Exit: Terminates program if DEBUG_MODE true.
 * Throws exception, otherwise.
 */
 
#define DEBUG_MODE true

inline void error(const string& gripe)
{
   if (DEBUG_MODE)
   {
      cerr << "Error, " << gripe << "!\n";
      exit(1); // terminate program
   }
   else // release mode
      throw runtime_error(gripe);
}

/*
 * Binary to string conversion.
 * ENTRY: operator<<() defined for Data
 */

template <typename Data>
string toString(const Data& val, bool terminate = false)
{
   ostringstream os;
   os << val;
   if (terminate) os << ends;
   return os.str();
}

/*
 * String to binary conversion
 * ENTRY: operator>>() defined for Data
 * EXIT: val holds binary value of str
 */

template <typename Data>
void fromString(Data& val, const string& str)
{
   istringstream is(str);
   is >> val;
}

Example: Improving the Reusable Interpreter

Now we can make the control loop function declared earlier into a reusable function. We begin by creating a file called cui.h:

/*
 * File:            cui.h
 * Programmer:      Pearce
 * Copyright (c):   2000, all rights reserved.
 */
#ifndef CUI_H
#define CUI_H
#include "util.h"

/*
 * Control loop for a console user interface (CUI)
 * Entry: assumes application-specific execute function
 * has been defined
 */
void controlLoop();

/*
 * Executes an application-specific command
 * Not implemented here
 */

string execute(string cmmd);

#endif

Only the control loop will be defined in cui.cpp. The implementation of execute can only be done when we know more about the application.

We improve our control loop by catching any exceptions thrown by execute calling the error function in release mode. Catching and throwing exceptions will be discussed in detail later.

/*
 * File:            cui.cpp
 * Programmer:      Pearce
 * Copyright (c):   2000, all rights reserved.
 */

#include "cui.h"

void controlLoop()
{
   string cmmd, result;
   cout << "type \"quit\" to quit\n";
   while(true)
   {
      cout << "-> "; // prompt user for a command
      cin >> cmmd;   // read command
      if (cmmd == "quit") break;
      // execute command & print result:
      try
      {
         result = execute(cmmd);
         cout << result << endl;
      }
      catch (runtime_error e)
      {
         // print error message & repair cin:
         cerr << e.what() << endl;
         cin.clear();
         cin.sync();
      }
   }
   cout << "bye\n";
}

Example: A Simple Calculator

Let's redo the simple calculator program using our new tools. Recall that the commands we need to execute are:

add ARG1 ARG2
mul ARG1 ARG2
sub ARG1 ARG2
div ARG1 ARG2

We might create a file called calc.cpp. This file completes the control loop by providing the missing implementation of execute:

/*
 * File:            calc.cpp
 * Programmer:      Pearce
 * Copyright (c):   2000, all rights reserved.
 */

#include "cui.h"

string execute(string cmmd)
{
   double arg1, arg2, result = 0;
   cin >> arg1;
   if (!cin) error("arguments must be numbers");
   cin >> arg2;
   if (!cin) error("arguments must be numbers");
  
   if (cmmd == "add") result = arg1 + arg2;
   else if (cmmd == "mul") result = arg1 * arg2;
   else if (cmmd == "sub") result = arg1 - arg2;
   else if (cmmd == "div")
   {
      if (!arg2) error("Error: can't divide by 0");
      result = arg1 / arg2;
   }
   else
   {
      error("Error: unrecognized command: " + cmmd);
   }
   return toString(result);
}

Notice that execute takes advantage of toString and error. This has the effect of further decoupling the user interface (control loop) from the application-specific logic (execute). Also notice that we are checking our input stream to make sure that it actually contains numbers and not something else.

Finally, we create main.cpp:

/*
 * File:            main.cpp
 * Programmer:      Pearce
 * Copyright (c):   2000, all rights reserved.
 */

#include "cui.h"
int main()
{
   controlLoop();
   return 0;
}

Here's a sample run:

type "quit" to quit
-> add 3 4
7
-> mul 2 5
10
-> div 5 0
Error, can't divide by 0!

Unfortunately, the program terminated abruptly at this point. This is because the DEBUG_MODE flag in util.h was set to true. If we set it to false and recompile, then we can survive errors like these:

type "quit" to quit
-> mul 2 5.3
10.6
-> div 9 0
can't divide by 0
-> mod 3 4
unrecognized command: mod
-> mul two nine
arguments must be numbers
-> sub 8 19
-11
-> quit
bye

Example: A Simple Calculator (version 2.0)

We can further decouple the user interface (control loop) from the application logic (execute), by giving the execute function the opportunity to parse the entire command line:

void controlLoop()
{
   string cmmd, result;
   cout << "type \"quit\" to quit\n";
   while(true)
   {
      cout << "-> "; // prompt user for a command
      getLine(cin, cmmd); // read entire command
      if (cmmd == "quit") break;
      try
      {
         result = execute(cmmd);
         cout << result << endl;
      }
      catch (runtime_error e)
      {
         cerr << e.what() << endl;
         cin.clear();
         cin.sync();
      }
   }
   cout << "bye\n";
}

The standard library does provide a global function called getline, which does what we want. Unfortunately, the VC++ version doesn't work. We'll need to provide our own. The following definition can be placed in cui.cpp just above the definition of controlLoop. We place the header in cui.h if we want to allow others to use it.

istream& getLine(istream& in, string& str)
{
   str = "";
   char c;
   do
   {
      c = in.get(); // extract next char (even white space!)
      if (c != '\n') str += c;
   }
   while (in && c != '\n');

   return in;
}

Our calculator's command language is too simple to really require a parser. The execute function converts the command line (cmmd) into an input string stream (stm), then uses the extraction operators (>>), to extract individual tokens. Next, else-if statements are used to determine the operator and compute the result. Of course the result returned must be a string, to the toString() function is called to perform the conversion.

string execute(string cmmd)
{
   double arg1, arg2, result = 0;
   string op;
   istringstream stm(cmmd);
   stm >> op;
   stm >> arg1;
   if (!stm) error("arguments must be numbers");
   stm >> arg2;
   if (!stm) error("arguments must be numbers");
   if (op == "add") result = arg1 + arg2;
   else if (op == "mul") result = arg1 * arg2;
   else if (op == "sub") result = arg1 - arg2;
   else if (op == "div")
   {
      if (!arg2) error("can't divide by 0");
      result = arg1 / arg2;
   }
   else
   {
      error("unrecognized command: " + op);
   }
   return toString(result);
}

Example: Vector Arithmetic

Let's use some of these tools in the context of an example: vector arithmetic. We will use the vectors in the standard library:

#include <vector>
#include <string>
#include <iostream>
using namespace std;

typedef vector<double> Vector;

We can hide the fact that vectors are C++ vectors by providing an inline dimension function that calls the size() function encapsulated by vectors:

inline int dim(const Vector& v) { return v.size(); }

Next, we define with a simple utility for displaying vectors. Note the use of default arguments and constant reference parameters:

ostream& write(const Vector& vec, ostream& os = cout)
{
   os << "( ";
   for(int i = 0; i < dim(vec); i++)
      os << vec[i] << ' ';
   os << ")\n";
   return os; // for nesting calls
}

The ostream parameter was passed by reference because the argument is modified internally. Characters were inserted into it.

We are now ready for add(). We don't use an else clause because then we would have to specify a bogus return value when the vector dimensions were different, even though error() will cause the program to terminate before the value is returned:

Vector add(const Vector& vec1, const Vector& vec2)
{
   if (dim(vec1) != dim(vec2))
      error("invalid input");
   Vector sum;
   for(int i = 0; i < dim(vec1); i++)
      sum[i] = vec1[i] + vec2[i];
   return sum;
}

Note that unlike C, C++ allows structures, i.e. non simple values, can be returned by functions. Here is a simple test harness. Note that C++ vectors allow us to specify their initial sizes when they are declared:

int main()
{
   const int size = 3;
   Vector a, b, c;
   for(int i = 0; i < size; i++)
   {
      a[i] = 2 * i + 1; // consecutive odds
      b[i] = 3 * i; // consecutive multiples of 3
   }
   c = add(a, b);
   write(a);
   write(b);
   write(c);
   return 0;
}

Here's the output produced:

( 1 3 5 )
( 0 3 6 )
( 1 6 11 )

Other vector arithmetic functions can be defined similarly.