Lab 12: 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:

One way to represent such values is using the string type:

cout << "\nEnter your gender: ";
string userGender;
cin >> userGender;
if (userGender == "male")
   // male computations
else if (userGender == "female")
  // female computations
else
  // error

For a few applications, this approach is acceptable. However, while it's easy to implement, it's not particular efficient. Comparing two string values is a slow operation since, in the worst case, every character in the two strings must be examined:

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;
  }
}
The actual definition of this operator in the string library might be a bit different, but it's close to this one.

Let's consider the work involved in the test (userGender == "male"):

  1. The literal "male" is converted to a string using the string constructor function, which makes a copy of the literal using a loop to do the copying.
  2. If the sizes are different, another loop is needed to compare all of the characters in the two strings.
  3. Plus, there are a whole bunch of book-keeping operations, but these are few compared to the two loops.
That's two loops, going through the entire strings. That's a lot of work for seeing if the gender is male.

In contrast, consider an equality test involving integers, (integer == 5):

  1. Call the basic machine instruction comparing integer and 5.

There's no question which one is faster. The integer comparison is a single machine instruction, but the string comparison is many, many, many machine instructions depending on the size of the strings.

So how can we use integer comparisons for this type of data? That's the question we answer in this lab.

Files

Directory: lab12

Create the specified directory, and copy the files above into the new directory. Only gcc users need a makefile; all others should create a project and add all of the .cpp files to it.

Add your name, date, and purpose to the opening documentation of the code and documentation files; if you're modifying and adding to the code written by someone else, add your data as part of the file's modification history.

This lab's exercise has eight parts. 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.

Part I: 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. 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 the identifier Season as the name of a new data type whose valid values are SEASON_UNDERFLOW, SPRING, SUMMER, AUTUMN, WINTER, and SEASON_OVERFLOW.

The "overflow" and "underflow" values provide values for error-handling and other unusual situations. We don't necessarily want to use them in our programs, but they'll be useful for detecting and handling error situations.

Given such a declaration, a programmer can write this:

Season aSeason = SPRING;
This declares a variable named aSeason as type Season and initializes it to SPRING.

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 };

This declaration creates NewType as the name of a new type whose valid values are Value1, Value2, ..., ValueN, which must be valid C++ identifiers, using the same conventions as for constant identifiers.

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 compile your code. When it is syntactically correct, continue to the next part.

Part II: 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;
This operator has not yet been defined. We can, however, provide such a definition.

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 enumeration-input function is as follows:

Specification:
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 corresponding enumerator.
return: in, for chaining.

Our function reads a string corresponding to an enumerator from in, determines the corresponding enumerator value, and passes back that enumerator value via the value parameter.

Here is an algorithm to solve this problem for our Season enumeration:

  1. Receive in and value.
  2. Read aString from in.
  3. Convert the characters in aString to upper-case, if need be.
  4. If aString equals "SPRING":
        value = SPRING.
    Else if aString equals "SUMMER":
        value = SUMMER.
    Else if aString equals "FALL" or aString equals "AUTUMN":
        value = AUTUMN.
    Else if aString equals "WINTER":
        value = WINTER.
    Else
        Display error message and terminate.
    End if.
  5. Return in.

Since we are comparing string values, we cannot use a switch statement, but must instead use a multi-branch if to implement this algorithm. The function can 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. Those double quotes make a world of a difference.

You may notice, though, that we're back where we started: comparing strings. Didn't we say this was really time consuming? Well, yes, it is. However, input comes to us only as chars. Fortunately, chars can be turned into a string which, in turn, can be turned into an enumerator. The key for all this work is that we do this conversion from string to enumerator once, at input. From then on, our code will use the more efficient enumerator. The majority of useful programs spend most of their time in non-input functions, and that's where it's most important to have the more efficient enumerator.

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

  1. Receive in and value.
  2. Read aString from in.
  3. Convert the characters in aString to upper-case, if need be.
  4. 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.
  5. 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, then continue on.

Part III: 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. Actually, we can write

cout << aGender;
But this will not print out what we want. We can, however, provide a definition that will print out useful output.

Similar to an istream, an ostream like cout is a stream of characters. We must display an enumerator as a sequence of characters, i.e., a string. This implies that our function must display the string that corresponds to the enumerator it receives. The specification of our function is thus as follows:

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

  1. Receive out and value.
  2. If value equals SPRING:
        Display "SPRING" via out.
    Else if value equals SUMMER:
        Display "SUMMER" via out.
    Else if value equals AUTUMN:
        Display "AUTUMN" via out.
    Else if value equals WINTER:
        Display "WINTER" via out.
    Else
        Display error message and terminate.
    End if.
  3. Return out.

Since we are comparing enumerator values, which are integer-compatible, we can use a switch statement to implement this algorithm. So, here's our function:

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;
}

The Season parameter is not passed by reference because it is a primitive type. It's perhaps less efficient to pass it by reference. This is unlike the class objects we passed into the output operator in the previous lab; then we did pass them by reference.

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

  1. Receive out and value.
  2. If value equals FEMALE:
        Display "FEMALE" via out.
    Else if value equals MALE:
        Display "MALE" via out.
    Else if value equals UNKNOWN:
        Display "UNKNOWN" via out.
    Else
        Display error message and terminate.
    End if.
  3. 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.

Part IV: 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:
Specification:
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 value. (This is unlike the postfix increment operator that we'll see below.) An algorithm to perform this operation for our Season enumeration is as follows:

  1. Receive value.
  2. If value equals SPRING:
        value = SUMMER.
    Else if value equals SUMMER:
        value = AUTUMN.
    Else if value equals AUTUMN:
        value = WINTER.
    Else if value equals WINTER:
        value = SEASON_OVERFLOW.
    Else
        Display an error message and terminate.
    End if.
  3. Return value.

Since we are comparing enumerators 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:

  1. Receive value.
  2. If value equals FEMALE:
        value = MALE.
    Else if value equals MALE:
        value = GENDER_OVERFLOW.
    Else
        Display an error message and terminate.
    End if.
  3. Return value.
Implement this algorithm by defining operator++ in Gender.cpp, and add prototypes of it to Gender.h and Gender.doc. Test what you have written by uncommenting the three lines in driver.cpp that refer to gender1. Then translate and run your program. Continue when it works correctly.

Part V: 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 << ' ';

The syntactic difference here is that the ++ increment operator is after (i.e., "post") the aSeason variable.

For this to work, we must provide a definition of the postfix increment operator ++, whose behavior is just slightly different from that of the prefix version:

Specification:
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.

It's identical to the specification for the prefix increment operator expect for one slight change to the return specification: return the original value of value. That's because the postfix increments the value after the return value is determined.

An algorithm to perform this operation for our Season enumeration is thus slighly different:

  1. Receive value.
  2. Set savedValue to be value.
  3. If value equals SPRING:
        value = SUMMER.
    Else if value equals SUMMER:
        value = AUTUMN.
    Else if value equals AUTUMN:
        value = WINTER.
    Else if value equals WINTER:
        value = SEASON_OVERFLOW.
    Else
        Display an error message and terminate.
    End if.
  4. Return savedValue.

Defining this function poses a syntax problem in C++. We've already used this prototype for the prefix increment operator:

Season operator++(Season & value);
But the postfix increment operators should have the same parameter list! So, C++ cheats---it has to. As a special case for a postfix increment operator, we add an extra (anonymous) int parameter like so:
Season operator++(Season & value, int );

We can then 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;
}
In odd situations like this where a parameter is needed, but not required by the function's definition, C++ allows us to forgo supplying a name for the parameter.

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

  1. Receive value.
  2. Set savedValue to value.
  3. If value equals FEMALE:
        value = MALE.
    Else if value equals MALE:
        value = GENDER_OVERFLOW.
    Else
        Display an error message and terminate.
    End if.
  4. Return savedValue.

Implement this algorithm to define a postfix version of operator++ in Gender.cpp. Add prototypes of it to Gender.h and Gender.doc. Test what you have written by uncommenting the three lines in driver.cpp that refer to gender3. Then translate and run your program. Continue when it works correctly.

Part VI: The Prefix Decrement Operator

Now if we envision incrementing an enumeration, it seems reasonable that we'll also want to decrement it. We might want to display the four seasons in reverse order by writing

for (Season aSeason = WINTER; aSeason >= SPRING; --aSeason)
  cout << aSeason << ' ';

To write this code, we must provide a definition of the prefix decrement operator --. The decrement operator is a dual to the increment operator, so the prefix decrement operator should look at lot like the prefix increment operator, just changing increments to decrements and overflows to underflows.

Since we've already gone through the prefix increment operator, we're going to let you try the prefix decrement operator on your own. First, establish a specification for a prefix decrement operator for an enumeration:

Question #12.1: Write down the specification for the prefix decrement operator for an enumeration.

Now the algorithm:

Question #12.2: Write an algorithm for the prefix decrement operator for the Gender enumeration.

Implement this algorithm by defining operator-- in Gender.cpp, and add prototypes of it to Gender.h and Gender.doc. Test what you have written by uncommenting the three lines in driver.cpp that refer to gender2. Translate and run your program. Continue when it works correctly.

Part VII: The Postfix Decrement Operator

Our final operation is the postfix decrement 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 << ' ';

Again, we've already seen the postfix increment operator, which is structurally similar (if not identical) to the postfix decrement operator. So the job is all yours:

Question #12.3: Write down the specification for the postfix decrement operator for an enumeration.

Now be a bit more specific for a Gender enumeration:

Question #12.4: Write an algorithm for the postfix decrement operator for the Gender enumeration.

Implement this algorithm in your code. This postfix decrement operator also takes an unused int parameter just like the postfix decrement operator.

Part VIII: A Code-Generating Tool for Enumerations

For most enumerations a programmer wants to use, the programmer follows these same steps:

  1. Declare the enumeration.
  2. Implement the input operation.
  3. Implement the output operation.
  4. Implement the prefix increment operation.
  5. Implement the postfix increment operation.
  6. Implement the prefix decrement operation.
  7. 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 any enumeration.

The process of defining these operations for a given enumeration is a mechanical one. (Hey, implement a few more enumerations if you don't believe us.) Aside from the particular enumerator values, the algorithms for each of these operations follow exactly the same structure. This process is so mechanical, it is straightforward to write 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, some languages automatically define the operations for you by the compiler whenever you create a new enumeration.

We have written a little program in 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.

Use a text editor to create an input file daysofweek.txt containing the days of the week:

sunday
monday
tuesday
wednesday
thursday
friday
saturday
Run enumGenerator; name the enumeration Day and use daysoftheweek.txt as the input file. 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!

Create a driver to test out the day tester (perhaps transliterate the driver for the Gender enumeration).

You may wish to study enumGenerate.cpp to see how it does what it does. 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, often one that the computer can do for you. Automated code generators of this sort are one way to solve such problems.

Submit

Submit all of the code for the Gender and Day enumerations (but not enumGenerate.cpp). Also turn in a sample execution of both drivers.

Terminology

enumerate, enumeration type, enumerator, prefix increment operator
Lab Home Page | Prelab Questions | Homework Projects
© 2003 by Prentice Hall. All rights reserved.
Report all errors to Jeremy D. Frens.