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"):
"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.strings.strings.
That's a lot of work for seeing if the gender is male.
In contrast, consider an equality test involving integers, (integer == 5):
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.
Directory: lab12
Gender.h, Gender.cpp, and
Gender.doc implement a gender enumeration.driver.cpp implements a driver.enumGenerator.cpp implements an enumeration
generator (covered later in the lab).Makefile is a makefile.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.
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:
enumNewType{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.
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, anistream;
value, an enumeration variable.
precondition:incontains thestringof a valid enumerator.
input: the enumerator-string fromin.
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:
in and value.aString from in.aString to upper-case, if need
be.aString equals "SPRING":
value = SPRING.aString equals "SUMMER":
value = SUMMER.aString equals "FALL" or aString
equals "AUTUMN":
value = AUTUMN.aString equals "WINTER":
value = WINTER.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:
in and value.aString from in.aString to upper-case, if need
be.aString == "FEMALE":
value = FEMALE.aString == "MALE":
value = MALE.aString == "UNKNOWN":
value = UNKNOWN.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.
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, anostream,value, an enumeration variable.
precondition:valuecontains a valid enumerator.
output: the string corresponding to that enumerator, viaout.
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:
out and value.value equals SPRING:
out.value equals SUMMER:
out.value equals AUTUMN:
out.value equals WINTER:
out.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:
out and value.value equals FEMALE:
out.value equals MALE:
out.value equals UNKNOWN:
out.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: Specification:receive:value, an enumeration object.
precondition:valuecontains 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:
value.value equals SPRING:
value = SUMMER.value equals SUMMER:
value = AUTUMN.value equals AUTUMN:
value = WINTER.value equals WINTER:
value = SEASON_OVERFLOW.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:
value.value equals FEMALE:
value = MALE.value equals MALE:
value = GENDER_OVERFLOW.value.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.
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:valuecontains a valid enumerator.
passback:value, containing the next enumerator in the enumeration (or its OVERFLOW value).
return: the original enumerator ofvalue.
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:
value.savedValue to be value.value equals SPRING:
value = SUMMER.value equals SUMMER:
value = AUTUMN.value equals AUTUMN:
value = WINTER.value equals WINTER:
value = SEASON_OVERFLOW.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:
value.savedValue to value.value equals FEMALE:
value = MALE.value equals MALE:
value = GENDER_OVERFLOW.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.
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.
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.
For most enumerations a programmer wants to use, the programmer follows these same steps:
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 saturdayRun
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 all of the code for the Gender and Day
enumerations (but not enumGenerate.cpp). Also turn in a
sample execution of both drivers.