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.string
s.string
s.
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
string
s.
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:in
contains thestring
of 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
string
s. Didn't we say this was really time consuming?
Well, yes, it is. However, input comes to us only as char
s.
Fortunately, char
s 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:value
contains 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: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:
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:value
contains 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.