Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

You Can Program In C++ (2006) [eng]

.pdf
Скачиваний:
80
Добавлен:
16.08.2013
Размер:
4.33 Mб
Скачать

148

CHAPTER 8

and run the program again. Making silk and nylon have the same value is probably not sensible in practice, but I have done so here to demonstrate that from the compiler’s point of view it does not constitute a problem.

Try adding other enumeration values (e.g. for wool, rayon, satin, etc.). TASK 8.1 Experiment with both allowing complete default values (i.e. not specifying any values in the definition) and choosing values. Make sure that you try negative integral values as well as positive ones. Continue until you are happy

that you understand how the values of enumeration constants are acquired.

We often find that we want to change the definition of an enum type by adding extra enumeration constants. When we do this, we generally do not want to disturb the values that have already been provided. Adding the new enumerators after the existing ones accomplishes that. However, there is often a catchall enumerator, such as other in the yarn example. Such catchall enumerators usually have either the smallest or the largest specified value. It is common practice to specify the value of such enumerators and insert new enumerators just before them in the definition. For example, suppose we define:

enum yarn {cotton, linen, silk, nylon, other = 127};

Now we can add a few enumerators without disturbing any of the existing values:

enum yarn {cotton, linen, silk, nylon, wool, satin, other = 127};

The reason I chose 127 for other is that it gives plenty of room for extra yarns but keeps within the values that can be provided by an 8-bit signed char. The significance of that design decision is that it allows the compiler to use that type as the underlying type if it is optimizing for space. (Unlike C, C++ allows the compiler to choose the underlying type used to store the enum’s values.)

Arithmetic and enum

C++ does not provide arithmetic operators for enum types, though it allows programmers to provide them. Their absence is often overlooked because the implicit conversion from an enum type to an integer makes it appear that they exist.

Try running this program:

TASK 8.2

#include

<iostream>

 

#include

<ostream>

enum yarn {cotton, linen, silk, nylon, other = 127};

int main( ){

std::cout << cotton + linen << '\n'; std::cout << silk * nylon << '\n'; std::cout << other / nylon << '\n'; std::cout << silk * 4 << '\n';

}

Please experiment until you feel confident that C++ will evaluate expressions involving enumerators (i.e. enumeration constants).

It is not obvious at this stage that the compiler is not doing arithmetic with the enumerators but is instead converting them to some kind of integer. We need a tool to investigate what is actually happening. C++ provides such a tool via the keyword typeid. That keyword needs some support from the C++ Standard

USER-DEFINED TYPES, PART 1: typedef AND enum

149

Library, so we need to #include <typeinfo> when we want to use it. Applying typeid to an expression results in a type info object. The type info type has a member function called name( ) that provides a

std::string value that indicates the type of the expression. The std::string value provided by calling name( ) on a type info object does not necessarily result in the same name that the programmer provided for the type, but in practice it often does. However, more importantly, it does provide the same result for expressions of the same type.

Try the following program:

TASK 8.3

#include <iostream> #include <ostream> #include <typeinfo>

enum yarn {cotton, linen, silk, nylon, other = 127};

int main( ){

std::cout << "The type of 'yarn' is "

<<typeid(yarn).name( ) << '\n'; std::cout << "The type of 'cotton' is "

<<typeid(cotton).name( ) << '\n'; std::cout << "The type of 'cotton + linen' is "

<<typeid(cotton + linen).name( ) << '\n'; std::cout << "The type of 'int' is "

<<typeid(int).name( ) << '\n';

std::cout << "The type of 'cotton || linen' is " << typeid(cotton || linen).name( ) << '\n';

}

Once again, experiment with other expressions until you feel able to predict the results. However, note that as soon as you attempt to do arithmetic with enumerators you get expressions whose type is not that of the enumerator.

The results of using enumerators for arithmetic are clearly reasonable. Trying to add two yarns together or multiplying a yarn by an integer makes no immediate sense (as a yarn). In other circumstances, some arithmetic operations might make sense for an enum type. We will see shortly that there are various ways to provide arithmetic with enum values.

We already know that enumerators implicitly convert to integer values when we use them in contexts where the numerical value can help. The following short program demonstrates that C++ does not allow conversions in the opposite direction.

Try to compile the following program:

TASK 8.4

#include <iostream> #include <ostream>

enum yarn {cotton, linen, silk, nylon, other = 127};

int main( ){

yarn thread1(cotton);

150

CHAPTER 8

thread1 = nylon; yarn thread2(4); thread1 = 4;

thread1 = cotton + silk;

}

Notice which lines generate errors and note the exact nature of the error.

The above task demonstrates that we can have variables of type yarn, but we can only initialize those variables with enumerators. In addition, the only assignments we are allowed are ones where the value being assigned has the correct type. We cannot assign an int value to a yarn variable.

Sometimes we want to be able to overrule the compiler, perhaps because we know that the numerical value correctly represents a value of the enum. The programmer can take responsibility for the conversion by using a cast. C++ has a well-designed hierarchy of casts (explicit conversions), but in the case of converting integer values to an enum value I find a modification of the C-style cast is sufficiently expressive. Here is the previous code amended so that it will compile and run:

int main( ){

yarn thread1(cotton); thread1 = nylon;

yarn thread2 = yarn(4); thread1 = yarn(4);

thread1 = yarn(cotton + silk);

}

The expressions yarn(4) and yarn(cotton + silk) are examples of function-style casts. They instruct the compiler to treat the value of the expression in the parentheses as a value of the type before the parentheses. In this case, they tell the compiler to treat 4 and cotton + silk as yarns. That is clear nonsense, but if the programmer wants to do that, the compiler will allow it, as long as the programmer takes responsibility by using a cast.

In the example code, the expressions I cast to yarn give values that are one of the enumerators. However, it is fair to ask what would happen if I wrote something such as:

thread1 = yarn(16);

The answer is that the compiler must accept such a statement as long as the value is within a permitted range. The rule for determining the permitted range is a little complicated but effectively says that you should first determine how many bits are needed to express all the enumerated values (in the case of yarn, that is 7, the number of bits needed to express 127). Any value expressible with that number of bits is allowed. The compiler is not required to diagnose attempts to use values outside the range; if the programmer uses any such value, the consequences can be anything (i.e. undefined behavior).

Operator Overloading

C++ allows programmers to redefine most operators as long as at least one of the operands is of a user-defined type. In other words, programmers are not allowed to redefine the meaning of an operator for a fundamental type. Not all operators can be redefined, and some of those that can have special conditions placed on the

USER-DEFINED TYPES, PART 1: typedef AND enum

151

contexts in which they can be redefined. We will be going into those in more detail in later chapters. However, I want to introduce you to the general principles of redefining an operator. C++ calls this process operator overloading, because the redefinition adds new meanings to existing operators.

The rules of C++ allow programmers to do completely silly things when overloading operators. However, just because you are allowed to do silly things does not mean that you should do so. The fundamental design guideline for overloading operators is that the new definition should not cause a domain expert any surprises. For example, if you redefine the + operator for your type the result should recognizably be some kind of addition.

We overload operators by writing functions with special names. These special names are composed of two tokens; the first is the keyword operator, and the second is the operator symbol.

Very few operators make any sense in the context of our yarn type. However, we might want to be able to write something such as

for(yarn i(cotton); i != other; ++i){ // process a type of yarn

}

to deal with all the possible values of yarn apart from the catchall other. If you try to compile code that includes the above loop, the compiler will indicate an error, because there is no available pre-increment operator for yarn. This is one of the cases where the implicit conversion of a yarn value to an int value will not help. The compiler can increment the resulting int value, but cannot write that value back to i because there is no implicit conversion from int to yarn.

We could rewrite the loop as:

for(yarn i(cotton); i != other; i = yarn(i + 1)){ // process a type of yarn

}

However, that introduces another problem because the loop will iterate over all the values from 0 to 126 inclusive, even though most of those values do not represent actual enumerators of yarn. We need a definition of pre-increment for yarn. Here is a possible one:

yarn & operator++(yarn & y){ if(y >= nylon) y = other;

else if(y < cotton) y = other; else y = yarn(y + 1);

return y;

}

Study that definition carefully. The first point to note is that it has a reference parameter: operator++ needs an lvalue for its operand, because it has to modify the stored value. The choice of a reference type for the return value is not essential but ensures that it works the same way that operator++ works for fundamental types. Unless there are special reasons to do otherwise, it is good practice to make operator overloads work like the built-in versions, as far as possible. Inside the body of the definition, we first check that the value being incremented is neither the value of the largest ordinary enumerator, nor already too big. If it is, we set the value to our catchall enumerator, other. Then we check to see that we do not have a value that is less than the smallest provided enumerator. If the provided value is too small, we set the result to the catchall value. In any other case we increment to the next enumerator. We assume that the enumerators are consecutive and do not include repeated values.

152

CHAPTER 8

T R Y T H I S

Try the following program that tests out our overloaded operator++ for yarn:

#include <iostream> #include <ostream>

enum yarn {cotton, linen, silk, nylon, other = 127};

yarn & operator++(yarn & y){ if(y >= nylon) y = other;

else if(y < cotton) y = other; else y = yarn(y + 1);

return y;

}

int main( ){

for(yarn i(cotton); i != other; ++i){ std::cout << i << '\n';

}

}

There are several problems with the above source code. The first is that any time we add a new enumerator to yarn we have to change the definition of operator++(yarn &). We should try to avoid this kind of maintenance problem. We can solve that problem by adding an extra enumerator to mark the end of our meaningful enumerators. For example:

enum yarn {cotton, linen, silk, nylon, end_of_yarns, other = 127};

We insert any extra enumerators directly before end of yarns; that way, end of yarns will always have a value that is one more than the largest legitimate value.

We next modify the definition of operator++(yarn &) to:

yarn & operator++(yarn & y){ if(y < cotton) y = other;

else if(y < (end_of_yarns - 1)) y = yarn(y + 1); else y = other;

return y;

}

Please note the changed logic of the definition. Make sure you understand how and why it does what we want. Also note that end of yarns is treated as a special case and is not treated as a yarn.

Verify that the modifications produce the same result as for the definition of TASK 8.5 main( ) used above. Try adding some more yarn enumerators and check that the program provides the correct output without any changes to the

definition of operator++(yarn &).

USER-DEFINED TYPES, PART 1: typedef AND enum

153

Another Overloaded Operator

Perhaps you are wondering whether we could get the output to provide a name instead of a number. To do this we need to do two things. First, we need to store the names of the yarns somewhere (the compiler converts the enumerators into binary for the benefit of the program and thereby discards the names we provided). Probably the simplest way to do this is by defining an array of strings to hold the names and initializing it with the names we are using:

char const * yarn_names[ ] = {"cotton", "linen", "silk", "nylon"};

C programmers will be familiar with how this definition works. For everyone else, the definition declares yarn names as an array (that is the [ ]) of pointers to const char. The use of empty square brackets is the way we instruct the compiler to use the number of initializers to determine the size of the array. The advantage is that the array size will automatically adjust if we add extra names. Currently we have provided four string literals as initializers (that is the significance of the pair of braces).

The second thing we need to do is to provide a new overload for operator<< which works when the left operand is a std::ostream (output) object and the right operand is a yarn. Here is a suitable definition:

std::ostream & operator<< (std::ostream & out, yarn const & y){ if(y < cotton) out << "unknown value";

else if(y < (end_of_yarns)) out << yarn_names[y]; else out << "unknown value";

return out;

}

TASK 8.6

 

Add the definition of yarn names and the definition of the overload of

 

 

 

 

 

operator<< to the source code for the previous task. Compile and run it to

 

 

check that the output is now a list of names rather than numbers.

 

 

Overloading the Input Operator

This one is rather more difficult because we have to deal with the problems of incorrect input. For example, what will we do if the user starts an otherwise valid name of a yarn with an uppercase letter? I am going to provide a bare-minimum implementation that assumes the user always provides valid input. I then invite you to add code to make the implementation more robust.

std::istream & operator>> (std::istream & in, yarn & y){ std::string input;

in >> input;

for(int i(cotton); i != end_of_yarns; ++i){ if(input == yarn_names[i]){

y=yarn(i); return in;

}

}

y=other;

}

154

CHAPTER 8

Type in the above definition for operator>>. Now use the following definition

TASK 8.7

of main( ) to test that the operator works as expected.

 

int main( ){

 

yarn y;

 

std::cout << "Please type in a yarn name: ";

 

std::cin >> y;

 

std::cout << y;

 

}

Now use the standard library function std::tolower to process the input so that the use of uppercase letters will be handled gracefully.

Note that the way I handled erroneous input means that any code that uses the overloaded operator>> can easily check whether the input was or was not a valid yarn name. In this case, we do not need to throw an exception to notify the user of invalid input: we can leave it to the user to check whether the variable being read into now contains the special enumerator value other. This is a perfectly reasonable way to handle erroneous input in this case. Do not get fixated on using exceptions as the only way to deal with problems.

EXERCISE

The purpose of the following is to provide you with some practical work that uses the ideas you have encountered in this chapter. It is important for your later progress that you complete this work because we will be building on it in future chapters.

Ordinary playing cards have two principle attributes: each one has a denomination and belongs to a suit. The denomination is one of ace, king, queen, jack, ten, nine, eight, seven, six, five, four, three, and two; the suit is one of club, diamond, heart, and spade. We could use the numbers from 0 to 51 to identify the 52 cards of a standard pack. For the time being, ignore the possibility of jokers. Some games use multiple packs. Either we can allow for repeated numbers in the range 0 to 51, or we can allow the use of higher numbers and reduce them modulo 52. That is, for a game using four full packs, we could either allow each of the values from 0 to 51 to occur four times, or we could make the range of numbers be 0 to 207. In the latter case, the first step in identifying a card will be to apply %52 to a card number to get down to the range 0 to 51.

Focusing on a single pack (i.e. with card values limited to 0 to 51), there are two strategies for determining attributes for a specific card value. We could imagine that the pack has been sorted so that the first 13 cards are the clubs in sequence, the next 13 are the diamonds, and so on. Now the result of dividing a card value by 13 will identify a suit and the result of using %13 (modulo 13) on the card value will identify the denomination.

Alternatively we could imagine sorting a pack so that the four aces are on top (in the order clubs, diamonds, hearts, spades), followed by the four twos in the same order, and so on. With that organization, we divide by 4 to determine the denomination and use %4 to determine the suit.

Of course, there are other logical ways to organize the 52 cards, as well as many illogical ones. I am only giving you some guidance so that you can focus on the main part of this exercise. You might also consider using a typedef to make card value a type name for some suitable integer type.

USER-DEFINED TYPES, PART 1: typedef AND enum

155

To complete this exercise you need to write code so that the main( ) below takes a number from 0 to 51 as input and outputs the name of the card that that number represents. For example, assuming you elect to organize your pack in suits rather than in denominations, 37 would represent the Queen of Hearts (37/13 = 2, 37 modulo 13 = 12), assuming that each suit is sorted as ace, two, three, . . . , jack, queen, king.

To achieve the desired end you will need to define a suitable enum for suits and another for denominations. You will need to provide a function that given a card value returns a suit value. You will need a second function that given a card value returns a denomination.

You will also need to overload operator<< for both suit and denomination so that the result of sending values of those types to an output stream is the desired word.

int main( ){ try{

int card(

read<int>("Please input a card value in the range 0 to 51")); card %= 52; // force into correct range

std::cout << card << " represents the " << get_denomination(card) << " of " << get_suit(card) << "s.\n";

}

catch(...){std::cerr << "An exception was thrown.\n"; }

}

You are free to modify this test program, which is only provided to help you focus on the design and implementation of the two enums.

S T R E T C H I N G E X E R C I S E

If you are an experienced programmer who wants to try something rather more difficult, try to add an operator>> for each of the two enums you provided for the previous exercise. Once you have done that, try to write a read card function that will extract a named card from input and convert that to a card value.

REFERENCE SECTION

typedef

The C++ keyword typedef is used to provide a synonym for an existing type. The existing type can be any fundamental type, derived type or user-defined type (including types derived from user-defined types). The main uses of typedef are to provide a type name that identifies the way a simple type is being used and to provide a simple name for a complicated type such as the type of a function.

C++ provides a number of typedef names in the Standard Library. The most common of these is size t, which is used as the name of whichever unsigned integer type an implementation uses for values representing the size (in bytes, i.e. unsigned char) of the memory footprint of a type

156

CHAPTER 8

or variable. Other Standard Library typedefs include time t and clock t, which name the types used for values representing time and clock ticks since program start.

There is a POSIX convention that typedef names end in t. Unfortunately, for reasons that seemed good to the designers, that convention is not always adhered to in C++. For example, wchar t is provided by a typedef in C but is a fundamental type in C++. There are also numerous places where C++ uses typedefs to assist with writing generic code where the names provided do not end in t.

The most important point to note about typedef is that it does not create a new type, just a new name for an existing type.

enum

C++ uses the keyword enum to create new integer types with a specified set of enumerated values. As well as the enumerated values, all values in a range determined by the number of bits needed to represent the enumerators (in binary) are valid values for the enum. The language provides for assignment of an enum value to a variable of the same enum type. It also provides an implicit conversion from an enum value to an int value. It does not supply any other operators for enum types, nor does it allow implicit conversion from an int value to an enum.

We must provide the enumerators for an enum as part of the definition of the enum. Unless explicitly stated as part of the definition, the first enumerator in the provided list will have zero as its value and subsequent enumerators will each have a value of one more than the immediately preceding one.

For example,

enum x {red, green, blue};

would result in red having a value of 0, green having a value of 1, and blue being 2, whereas

enum color {red = 1, green, blue = 4};

would result in red being 1, green being 2, and blue being 4.

The language allows more than one enumerator to have the same value. For example,

enum color {red = 1, crimson = 1, green, blue = 4, azure = 4};

is all right as far as the C++ language is concerned.

Note that the final semicolon is required to end a definition of a new type. Strictly speaking, we could declare a variable or function name between the closing brace of the definition and the semicolon that closes the declaration statement. However, no experienced programmer would use that facility today, though C programmers often used it in days gone by.

Because enum provides a true type rather than just a type name, C++ allows the programmer to define meanings for most of the language’s operators when applied in a context with at least one operand of the enum type. Using the ability to overload operators for enum types is not very common but, as long as you limit yourself to places where it makes sense, it is a useful tool for writing clearer source code.

C H A P T E R 9

User-Defined Types, Part 2: Simple classes (value types)

The class is a key design and development tool in C++. It comes in several forms and, for historical reasons, three keywords are used to provide user-defined class types. These are struct (inherited from C), class (introduced in the early development of ‘C with classes’, which was to become C++), and union (a restricted form of class used to minimize memory usage on some systems that have very limited resources). union user-defined types are rare these days; they are not generally used except in memory-constrained embedded programming and possibly in low-level library design.

The class concept has two major branches: value types and entity types. C++ uses the same mechanisms for both, which burdens the programmer with understanding the difference. A typical value type is one where you would naturally use copies for arguments when calling a function or for returning data from a function. An entity type is one where copying would normally be an error, and the natural usage would be to use references for both parameters and return from a function. We normally talk about the ‘state’ of an entity rather than its ‘value’. We reserve the latter term for value types. Another way of viewing this distinction is that the identity of an instance of an entity type is significant, whereas only the stored representation is significant for a pure value type. If you find these distinctions hard to understand, wait till you have more experience and then come back to them.

Some languages force us to treat everything as an entity; some languages try to provide a clear separation between value types and entity types. In reality, few things are purely values or purely entities; the context of use needs consideration. The major distinction is in how we choose to use them. Java insists that all the fundamental types are value types and fails to provide a mechanism for using an instance of a fundamental type as an entity. Smalltalk insists that everything is an entity, which leaves us with problems for situations where we clearly want a value. C++ leaves it up to programmers to use things the way they want to. That places a heavy burden on the programmer to select the right behavior. We will see in the next chapter that C++ provides a mechanism for switching off copy semantics (behavior), which results in preventing the resulting type from being a value type – value types are inherently copiable. However, it is not wrong for an entity type to have copy semantics.

There is very little technical difference between declaring user-defined types with the class keyword as opposed to struct. We will see later that it is customary to limit the use of struct to highlight a small subgroup of user-defined types whose data is accessible in the scope of the definition of the type.

We already know that a type usually consists of two elements: memory, which can store a bit pattern representing a value or state; and behavior, which specifies how values of the type can be used and modified. In general, it is only the second element that is of concern to the user of a type. You generally have no need to know how values are stored; you only need to know what you can do with them.