It is often the case that a programmer needs to represent some real-world object whose possible values are non-numeric. For example:
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
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."
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.
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.
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.
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.
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.
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.
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.
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.
Enumeration, Enumerator, Prefix Increment Operation, Postfix Increment Operation, Prefix Decrement Operation, Postfix Decrement Operation, Code Generation.
Hard copies of Gender.h, Gender.cpp, Gender.doc, and driver.cpp, an execution record showing its execution, plus a copy of Day.h.
Forward to the Homework Projects