Lab 11: Building Enumerations


Introduction

It is often the case that a programmer needs to represent some real-world object whose possible values are non-numeric. For example:

and so on. One way to represent such values is using the string type:
   cout << "\nEnter your gender: ";
   string userGender;
   cin >> userGender;
   if (userGender == "male")
      // ...
For many applications, this approach is perfectly acceptable. However, for applications where speed is important, this approach has a major drawbacks -- comparing two string values is a slow operation. That is, the string equality operation is typically defined something like this:
   bool operator==(const string & str1, const string & str2)
   {
      if (str1.size() != str2.size())
         return false;
      else
      {
         for (int i = 0; i < str1.size(); i++)
            if (str1[i] != str2[i])
               return false;

         return true;
      }
   }
If we trace the execution through a call to this function:
   (userGender == "male")
then
  1. the literal "male" is converted to a string using the string constructor function, which copies that literal (i.e., using a loop to do the copying);
  2. the size() of userGender is compared against the size() of the string containing "male";
  3. the loop control variable i is initialized to zero; and
  4. the loop control variable is compared against userGender.size();
before we finally get to where the first character in userGender is compared to the first character in "male". If the two strings are different, then the function may return false at that point, but if the user entered male, then three more trips through the loop are required to verify that each character in str1 is the same as the corresponding character in str2. Needless to say, all of this consumes valuable time, so if time is important in a problem, a different approach is needed. Today's exercise presents one way to solve this problem.

Getting Started

Create a new project in which to store the work for this exercise. Then save copies of Gender.h, Gender.cpp, Gender.doc, and driver.cpp in the project directory, and add the .cpp files to your project.

Today's exercise has eight parts, which we will do in sequence. The program in driver.cpp tests each part. Open driver.cpp and take a few moments to study it, noting that most of the program is at present "commented out."

1. Declaring An Enumeration Type

To provide a way to use real-world names in a program without using a string, C++ allows a programmer to declare an enumeration type, or an enum for short. As its name implies, an enumeration is a type in which the programmer enumerates (i.e. exhaustively lists) all of the values for that type.

For example, suppose that we want to create a new type named Season, with the values: Spring, Summer, Autumn and Winter. We can do so be creating a Season enumeration, as follows:

   enum Season {SEASON_UNDERFLOW, SPRING, SUMMER, 
                 AUTUMN, WINTER, SEASON_OVERFLOW};
With this statement, we are declaring to the compiler that the identifier Season is the name of a new type, whose valid values are SEASON_UNDERFLOW, SPRING, SUMMER, AUTUMN, WINTER, and SEASON_OVERFLOW. (The "overflow" and "underflow" values provide "abnormal" values for error-handling and other unusual situations.)

Given such a declaration, a programmer can write:

   Season aSeason = SPRING;
to declare a variable named aSeason and initialize it to SPRING. Note that since an enumeration is not a string, no quotation marks are needed when you use an enumeration value like SPRING, which is called an enumerator. Since they are essentially constant values, it is conventional to write enumerators in uppercase.

The general pattern for declaring an enumeration with N values is thus:

   enum NewType {Value1, Value2, ... ValueN};
Such a declaration creates NewType as the name of a new type, whose valid values are Value1, Value2, ..., ValueN, which must be C++ identifiers.

Using this information, open Gender.h and replace the appropriate comment with a statement that declares a new type named Gender, whose valid values are GENDER_UNDERFLOW, FEMALE, MALE, UNKNOWN, and GENDER_OVERFLOW. To check the syntax of what you have written, "uncomment" the declaration statement in driver.cpp and translate your program using the provided Makefile. When it is syntactically correct, continue to the next part.

2. The Input Operation

In order to input an enumeration, it is important to recognize that the input operator (>>) only works for a type if it has been defined for that type. That is, we cannot simply write

   cin >> aGender;
and expect to read a Gender value from the keyboard, because there is no definition of the input operator for type Gender. We can, however, provide such a definition.

In designing such a function, it is important to remember that an istream like cin delivers a stream of characters. That is, we must read an enumerator as a sequence of characters (i.e., a string) and convert that string into the corresponding enumerator. The specification of our function is thus as follows:

   Receive: in, an istream,
            value, an enumeration variable.
   Precondition: in contains the string of a valid enumerator.
   Input: the enumerator-string from in.
   Passback: in, with the enumerator-string extracted from it;
             value, containing the enumerator.
   Return: in, for chaining.
Our function must thus read a string corresponding to an enumerator from in, determine the corresponding enumerator value, and pass back that enumerator value via parameter value. Here is an algorithm to solve this problem for our Season enumeration:
   0. Receive in and value.
   1. Read aString from in.
   2. Convert the characters in aString to upper-case, if need be.
   3. If aString == "SPRING":
         value = SPRING.
      Else if aString == "SUMMER":
         value = SUMMER.
      Else if aString == "FALL" OR aString == "AUTUMN":
         value = AUTUMN.
      Else if aString == "WINTER":
         value = WINTER.
      Else
          Display error message and terminate.
      End if.
   4. Return in.
Note that since we are comparing string values, we cannot use a switch statement, but must instead use an if-else-if statement to implement this algorithm. This function can thus be defined as follows:
   istream & operator>>(istream & in, Season & value)
   {
      string aString;
      in >> aString;

      for (int i = 0; i < aString.size(); i++)
         if (islower(aString[i]))
            aString[i] = toupper(aString[i]);

      if (aString == "SPRING")
         value = SPRING;
      else if (aString == "SUMMER")
         value = SUMMER;
      else if (aString == "AUTUMN" || aString == "FALL")
         value = AUTUMN;
      else if (aString == "WINTER")
         value = WINTER;
      else
      {
         cerr << "\n*** Invalid enumerator: " << aString
              << " received by >>\n" << endl;
         exit(1);
      }

      return in;
   }
Note in particular the difference between a string literal and an enumerator. The C++ compiler uses the double-quotes surrounding "SPRING" to distinguish it from an enumerator like SPRING. If you were to try and assign "SPRING" to value (or assign SPRING to aString), the compiler would generate an error, since the types of the objects do not match.

Using this definition as a model, define operator>> for your Gender enumeration in Gender.cpp, so that it implements the following algorithm:

   0. Receive in and value.
   1. Read aString from in.
   2. Convert the characters in aString to upper-case, if need be.
   3. If aString == "FEMALE":
         value = FEMALE.
      Else if aString == "MALE":
         value = MALE.
      Else if aString == "UNKNOWN":
         value = UNKNOWN.
      Else
          Display error message and terminate.
      End if.
   4. Return in.
Add a prototype for this function in the appropriate places in Gender.h and Gender.doc. Then "uncomment" the input step in driver.cpp and test your function by translating your driver program. When it is syntactically correct, continue to the next part of the exercise.

3. The Output Operation

As with the input operator, the output operator cannot be applied to an object unless it has been defined for objects of that type. That is, we cannot simply write

   cout << aGender;
and expect to display a Gender value from the keyboard, because there is no predefined definition of the output operator for type Gender. We can, however, provide such a definition.

As we saw with an istream, it is important to remember that an ostream like cout delivers a stream of characters. That is, we must display an enumerator as a sequence of characters (i.e., a string), which implies that our function must display the string that corresponds to the enumerator it receives. The specification of our function is thus as follows:

   Receive: out, an ostream,
            value, an enumeration variable.
   Precondition: value contains a valid enumerator.
   Output: the string  corresponding to that enumerator, via out.
   Passback: out, containing the enumerator-string.
   Return: out, for chaining.
Our function must thus determine the string that corresponds to the enumerator it receives from the caller, and display that string. Here is an algorithm to solve this problem for our Season enumeration:
   0. Receive out and value.
   1. If value == SPRING:
         Display "SPRING" via out.
      Else if value == SUMMER:
         Display "SUMMER" via out.
      Else if value == AUTUMN:
         Display "AUTUMN" via out.
      Else if value == WINTER:
         Display "WINTER" via out.
      Else
          Display error message and terminate.
      End if.
   2. Return out.
Note that since we are comparing enumerator values, which are integer-compatible, we can use a switch statement to implement this algorithm. This function can thus be defined as follows:
   ostream & operator<<(ostream & out, const Season value)
   {
      switch(value)
      {
         case SPRING:
            out << "SPRING";
            break;
         case SUMMER:
            out << "SUMMER";
            break;
         case AUTUMN:
            out << "AUTUMN";
            break;
         case WINTER:
            out << "WINTER";
            break;
         default:
            cerr << "\n*** Invalid enumerator received by <<\n" << endl;
            exit(1);
      }

      return out;
   }
Using this definition as a model, define operator<< for your Gender enumeration in Gender.cpp, so that it implements the following algorithm:
   0. Receive out and value.
   1. If value == FEMALE:
         Display "FEMALE" via out.
      Else if value == MALE:
         Display "MALE" via out.
      Else if value == UNKNOWN:
         Display "UNKNOWN" via out.
      Else
          Display error message and terminate.
      End if.
   2. Return out.
Add a prototype for this function in the appropriate place in Gender.h and Gender.doc. Then "uncomment" the first and last output steps in driver.cpp and test your function by translating your driver program. When it is syntactically correct, continue to the next part of the exercise.

4. The Prefix Increment Operator

It is often convenient if we can increment an enumeration variable. For example, we might want to display the four seasons by writing

   for (Season aSeason = SPRING; aSeason <= WINTER; ++aSeason)
      cout << aSeason << ' ';
To write a statement like this, we must provide a definition of the prefix increment operator ++. We can specify its behavior as follows:
   Receive: value, an enumeration object.
   Precondition: value contains a valid enumerator.
   Passback: value, containing the next enumerator in the enumeration
                             (or its OVERFLOW value).
   Return: value.
The thing to remember about the prefix increment operator is that its return-value is the incremented variable. An algorithm to perform this operation for our Season enumeration is thus as follows:
   0. Receive value.
   1. If value == SPRING:
         value = SUMMER.
      Else if value == SUMMER:
         value = AUTUMN.
      Else if value == AUTUMN:
         value = WINTER.
      Else if value == WINTER:
         value = SEASON_OVERFLOW.
      Else
         Display an error message and terminate.
   2. Return value.
Since we are comparing enumeration values we can implement this algorithm using a switch statement:
   Season operator++(Season & value)
   {
      switch (value)
      {
         case SPRING:
            value = SUMMER;
            break;
         case SUMMER:
            value = AUTUMN;
            break;
         case AUTUMN:
            value = WINTER;
            break;
         case WINTER:
            value = SEASON_OVERFLOW;
            break;
         default:
            cerr << "\n*** Invalid enumerator received by prefix++\n" << endl;
            exit(1);
      }

      return value;
   }
The algorithm for a version of this operation for the Gender enumeration is similar:
   0. Receive value.
   1. If value == FEMALE:
         value = MALE.
      Else if value == MALE:
         value = GENDER_OVERFLOW.
      Else
         Display an error message and terminate.
   2. Return value.
Using this information, implement this algorithm by defining operator++ in Gender.cpp, and add prototypes of it to Gender.h and Gender.doc. Then test what you have written by "uncommenting" the three lines in driver.cpp that refer to gender1; and then translating and running your program. Continue when it works correctly.

5. The Postfix Increment Operator

While overloading the prefix operator provides us with the means of incrementing an enumeration, it is also sometimes desirable to be able to use the postfix increment operation. For example, we might want to display the four seasons by writing

   for (Season aSeason = SPRING; aSeason <= WINTER; aSeason++)
      cout << aSeason << ' ';
To write a statement like this, we must provide a definition of the postfix increment operator ++, whose behavior is slightly different from that of the prefix version:
   Receive: value, an enumeration object.
   Precondition: value contains a valid enumerator.
   Passback: value, containing the next enumerator in the enumeration
                             (or its OVERFLOW value).
   Return: the original enumerator of value.
That is, both this and the prefix version pass back the next enumerator via parameter value; however, where the prefix version returns that next enumerator, the postfix version returns the original enumerator of value. An algorithm to perform this operation for our Season enumeration is thus slighly different:
   0. Receive value.
   1. Save value in savedValue.
   2. If value == SPRING:
         value = SUMMER.
      Else if value == SUMMER:
         value = AUTUMN.
      Else if value == AUTUMN:
         value = WINTER.
      Else if value == WINTER:
         value = SEASON_OVERFLOW.
      Else
         Display an error message and terminate.
   3. Return savedValue.
The definition and prototype of this function must be distinct from those of the prefix version of operator++. Put differently, the signatures (the list of parameter types) of the prefix and postfix versions of the function must be different, in order for the compiler to tell them apart. The convention adopted by C++ is to add an unused int parameter to the postfix version, to distinguish it from the prefix version. That is, the C++ compiler will associate the prototype
   Season operator++(Season & value);
with the prefix increment operator, and will associate the prototype
   Season operator++(Season & value, int );
with the postfix increment operator. We can thus define this function for our Season enumeration as follows:
   Season operator++(Season & value, int )
   {
      Season savedValue = value;

      switch (value)
      {
         case SPRING:
            value = SUMMER;
            break;
         case SUMMER:
            value = AUTUMN;
            break;
         case AUTUMN:
            value = WINTER;
            break;
         case WINTER:
            value = SEASON_OVERFLOW;
            break;
         default:
            cerr << "\n*** Invalid enumerator received by postfix++\n" << endl;
            exit(1);
      }

      return savedValue;
   }
Note that for such odd situations where a function requires a parameter that is unused by the function's definition, C++ permits you to omit the name of that parameter in the function definition.

The algorithm for a version of this operation for the Gender enumeration is similar:

   0. Receive value.
   1. Save value in savedValue.
   2. If value == FEMALE:
         value = MALE.
      Else if value == MALE:
         value = GENDER_OVERFLOW.
      Else
         Display an error message and terminate.
   3. Return savedValue.
Using this information, implement this algorithm by defining a postfix version of operator++ in Gender.cpp, and add prototypes of it to Gender.h and Gender.doc. Then test what you have written by "uncommenting" the three lines in driver.cpp that refer to gender3; and then translating and running your program. Continue when it works correctly.

6. The Prefix Decrement Operator

Just as we sometimes want to increment, other times we want to decrement. For example, we might want to display the four seasons in reverse order by writing

   for (Season aSeason = WINTER; aSeason >= SPRING; --aSeason)
      cout << aSeason << ' ';
To write a statement like this, we must provide a definition of the prefix decrement operator --. We can specify its behavior as follows:
   Receive: value, an enumeration object.
   Precondition: value contains a valid enumerator.
   Passback: value, containing the previous enumerator in the enumeration
                             (or its UNDERFLOW value).
   Return: value.
The thing to remember about the prefix decrement operator is that its return-value is the decremented variable. An algorithm to perform this operation for our Season enumeration is thus as follows:
   0. Receive value.
   1. If value == SPRING:
         value = SEASON_UNDERFLOW.
      Else if value == SUMMER:
         value = SPRING.
      Else if value == AUTUMN:
         value = SUMMER.
      Else if value == WINTER:
         value = AUTUMN.
      Else
         Display an error message and terminate.
   2. Return value.
Since we are comparing enumeration values we can implement this algorithm using a switch statement:
   Season operator--(Season & value)
   {
      switch (value)
      {
         case SPRING:
            value = SEASON_UNDERFLOW;
            break;
         case SUMMER:
            value = SPRING;
            break;
         case AUTUMN:
            value = SUMMER;
            break;
         case WINTER:
            value = AUTUMN;
            break;
         default:
            cerr << "\n*** Invalid enumerator received by prefix--\n" << endl;
            exit(1);
      }

      return value;
   }
The algorithm for a version of this operation for the Gender enumeration is similar:
   0. Receive value.
   1. If value == FEMALE:
         value = GENDER_UNDERFLOW.
      Else if value == MALE:
         value = FEMALE.
      Else
         Display an error message and terminate.
   2. Return value.
Using this information, implement this algorithm by defining operator-- in Gender.cpp, and add prototypes of it to Gender.h and Gender.doc. Then test what you have written by "uncommenting" the three lines in driver.cpp that refer to gender2; and then translating and running your program. Continue when it works correctly.

7. The Postfix Decrement Operator

Our final operation is the postfix increment operation. For example, we might want to display the four seasons in reverse order by writing

   for (Season aSeason = WINTER; aSeason >= SPRING; aSeason--)
      cout << aSeason << ' ';
To write a statement like this, we must provide a definition of the postfix decrement operator --, whose behavior is slightly different from that of the prefix version:
   Receive: value, an enumeration object.
   Precondition: value contains a valid enumerator.
   Passback: value, containing the previous enumerator in the enumeration
                             (or its UNDERFLOW value).
   Return: the original enumerator of value.
That is, both this and the prefix version pass back the next enumerator via parameter value; however, where the prefix version returns that previous enumerator, the postfix version returns the original enumerator of value. An algorithm to perform this operation for our Season enumeration is thus slighly different:
   0. Receive value.
   1. Save value in savedValue.
   2. If value == SPRING:
         value = SEASON_UNDERFLOW.
      Else if value == SUMMER:
         value = SPRING.
      Else if value == AUTUMN:
         value = SUMMER.
      Else if value == WINTER:
         value = AUTUMN.
      Else
         Display an error message and terminate.
   3. Return savedValue.
As before, the definition and prototype of this function must be distinct from those of the prefix version of operator--. Put differently, the signatures (the list of parameter types) of the prefix and postfix versions of the function must be different, in order for the compiler to tell them apart. The same convention is used to distinguish the prefix and postfix versions of the decrement operator as was used to distinguish the versions of the input operator. We can thus define this function for our Season enumeration as follows:
   Season operator--(Season & value, int )
   {
      Season savedValue = value;

      switch (value)
      {
         case SPRING:
            value = SEASON_UNDERFLOW;
            break;
         case SUMMER:
            value = SPRING;
            break;
         case AUTUMN:
            value = SUMMER;
            break;
         case WINTER:
            value = AUTUMN;
            break;
         default:
            cerr << "\n*** Invalid enumerator received by postfix++\n" << endl;
            exit(1);
      }

      return savedValue;
   }
The algorithm for a version of this operation for the Gender enumeration is similar:
   0. Receive value.
   1. Save value in savedValue.
   2. If value == FEMALE:
         value = GENDER_UNDERFLOW.
      Else if value == MALE:
         value = FEMALE.
      Else
         Display an error message and terminate.
   3. Return savedValue.
Using this information, implement this algorithm by defining a postfix version of operator-- in Gender.cpp, and add prototypes of it to Gender.h and Gender.doc. Then test what you have written by "uncommenting" the three lines in driver.cpp that refer to gender4; and then translating and running your program. Continue when it works correctly.

8. A Code-Generating Tool for Enumerations

For most enumerations a programmer wants to use, these same seven steps are needed: (i) declare the enumeration; (ii) implement the input operation, (iii) implement the output operation, (iv) implement the prefix increment operation, (v) implement the postfix increment operation, (vi) implement the prefix decrement operation, and (vii) implement the postfix decrement operation. Some enumerations may require additional operations (e.g., a DaysIn() function would be useful for a Month enumeration), but these six operations are a minimal group needed by all enumerations.

Since you have just implemented one enumeration, it may not be evident, but the process of defining these operations for a given enumeration is a mechanical one -- aside from the particular enumerator values, the algorithms for each of these operations is virtually the same from one enumeration to another. This process is so mechanical, it is straightforward to devise a program that, given a sequence of enumerators and the desired name of the enumeration, generates the C++ code to declare the enumeration type and its operations. (In fact, there are other languages where these operations are automatically defined for you by the compiler, whenever you create a new enumeration.)

Now that you have seen how to declare an enumeration and define its operations, it seems unconscionable to make you mechanically go through these same seven steps each time you want to define a new enumeration. As a result, we have written a little program named enumGenerator.cpp that, given the name of an enumeration and a sequence of enumerators stored (one per line) in a file, generates the header, implementation, and documentation files for that enumeration.

Save a copy of enumGenerator.cpp in your directory and translate it. Then use the text editor to create an input file containing the days of the week (i.e., sunday, monday, tuesday, wednesday, thursday, friday, saturday), one per line. Run enumGenerator, telling it to name the enumeration Day, and giving it the input file containing the names of the days of the week. enumGenerator should then generate the files Day.h, Day.cpp, and Day.doc. Take a few moments to look over these files, and see how much code was just automatically generated for you!

To test the correctness of this code, save a copy of dayTester.cpp in your directory, translate and run it. You may wish to study enumGenerate.cpp to see how it does what it does, since you will likely run into similar situations in the future where taking the time to write a code-generating program will be a worthwhile investment of your time.

Remember, whenever you find yourself doing the same thing over and over, look for a better way! Automated code generators of this sort are one way to solve such problems.

Phrases you should now understand:

Enumeration, Enumerator, Prefix Increment Operation, Postfix Increment Operation, Prefix Decrement Operation, Postfix Decrement Operation, Code Generation.


Submit:

Hard copies of Gender.h, Gender.cpp, Gender.doc, and driver.cpp, an execution record showing its execution, plus a copy of Day.h.


Back to This Lab's Home Page

Back to the Prelab Questions

Forward to the Homework Projects


Copyright 1998 by Joel C. Adams. All rights reserved.