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

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

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

138

CHAPTER 7

EXERCISES

7.Modify the above code so that you can output a sorted list of double values that have been supplied by keyboard input.

8.Write a program that collects a list of words from the keyboard using fgw::read<>( ) and then outputs the words in alphabetical order.

9.Modify the previous program to list the words input in reverse alphabetical order.

10.Write a program that prompts the user for two integer values and a basic arithmetical operation (+, -, *, or /) and outputs the correct answer. You will probably find a switch statement useful for this program as well as using fgw::read<> for input.

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

11.The header file line drawing.h for my library provides various functions for drawing lines in the Playpen window. One of the provided functions can be used as if we had declared:

void drawline(fgw::playpen & p,

int beginx, int beginy, int endx, int endy);

(There is a defaulted parameter, of which we will learn more much later.) If paper is an fgw::playpen object, then the statement

fgw::drawline(paper, -10, -5, 20, 30);

will draw a black line from (−10, −5) to (20, 30).

Remembering that the Playpen is only updated when the display function is applied to the fgw::playpen object, write a program that will prompt you for coordinates (pairs of integer values) and then join each new pair (after the first one) to the previous one. Choose some suitable way to end the program.

12.If you want other colors for lines you will need to use the version of fgw::drawline( ) that can accept an fgw::hue argument after the coordinates. For example,

fgw::drawline(paper, -30, -12, 50, 13, fgw::red4);

will create a medium red line from (−30, −12) to (50, 13).

Modify your program from Exercise 11 so that the user is prompted for a color value (range 0 to 255) as well as for the next point. It should then draw the line from the previous point in the specified color.

Note that you will need to use fgw::read<int> to get the color because fgw::read<fgw::hue> does not work as you might expect.

GENERIC FUNCTIONS

139

REFERENCE SECTION

Function templates provide a mechanism for providing type-independent solutions to problems. A function template has two distinct sets of parameters. The first one is a non-empty list of template parameters. The second is an ordinary, perhaps empty, list of function parameters whose types may be determined by the arguments supplied to the template parameters. The return type of a function template may also depend on the template parameters.

A template parameter may be of one of three types:

template type parameter: This is specified by using either typename or class followed by the name that will be a synonym for the type argument in the remainder of the template declaration.

template value parameter: A limited number of types (mainly integer types) may be used as template value parameters. I have not covered these in this chapter, but we will see examples of such template parameters in later chapters.

template template parameter: These are way out of the scope of an introductory C++ book and are only mentioned here for completeness.

Sometimes a type that depends on a template type will be needed. In such cases the compiler requires notification by prefixing the relevant use of a type with the typename keyword. We had an example of this when we used iterators; the iterator type for a vector is a dependent type because it is a type that is defined as part of the definition of a vector. This concept will become clearer when we deal with user-defined types (i.e. types that are neither fundamental C++ types nor derivatives of those types.

The standard form for a function template declaration is:

template<parameter-list>

return-type function-name(parameter-list);

And the form for a function template definition is:

return-type function-name(parameter-list){ body-of-function-template

}

It is usual to provide the function template definition in a header, because the compiler will need the definition to generate code for the specific template arguments used in the programmer’s code. There are advanced uses of templates where bare declarations are useful.

Template Type Argument Deduction

It is often possible for the compiler to deduce the template type arguments from the function arguments when a template is used for a function call. When the compiler is left to deduce the template arguments the deduction must not be ambiguous. For example, if two function arguments have the same template type they must have the same exact type in the function call. We had examples of this restriction in the max( ) example.

However, if the programmer elects to explicitly specify the template arguments, the normal process of conversions and promotions will be applied to the function arguments. For this reason, explicit specification of template type arguments should be done with care: the compiler will try to comply with the programmer’s choice even if that was unwise.

140

CHAPTER 7

Specialization

Sometimes it is necessary to provide special handling for a type. For example, we might have a function template that needs to handle the std::string type differently from other types. We had an example of this when we specialized the max( ) function template so that we could select the ‘larger’ of two std::string values disregarding the case of the letters.

The syntax for a function-template specialization is:

template<>

return-type function-name<argument-list>(parameter-list){ code-for-this-special-case

}

Note that a specialization is identified by the use of an empty template parameter list and the provision of an explicit template argument list after the function name.

Overloading

More than one function template can share a name (just as more than one function can share a name) as long as the template parameter lists are different. If there is more than one function template with the same name visible to the compiler, it will attempt to determine from the context which one the programmer intends to use. If the compiler cannot determine a unique choice it will issue an ambiguity error and pass the problem back to the programmer, who will now have to resolve the ambiguity. In our example we could resolve the ambiguity by providing a fully elaborated name (i.e. by saying which namespace we want the compiler to use).

Function templates can also share their name with ordinary functions. In such cases the compiler will select an ordinary function whose parameter types exactly match the argument types provided by the call in preference to a function generated from a template.

If the template parameters have been made explicit, then only a function template can be selected. In the case where two function templates can generate code from the given explicit template arguments, the compiler will attempt to resolve the conflict by using a set of rules defined by the C++ Standard. These rules are notoriously difficult to understand and so I will not attempt to describe them here. If such code becomes important to you, you will need to study an advanced text on templates such as C++ Templates [Vandevoorde & Josuttis 2002].

Templated Return Type

Sometimes we want to overload a function on its return type. In general the language does not support this, because it is not possible to select from a set of candidate functions based only on a return type. However, we do sometimes want to use the same name for functions that only differ in their return type. In such cases we can use a function template coupled with explicit provision of the template arguments.

The read<> function templates from my library are an example of this kind of use of function templates.

Function Templates and the Standard C++ Library

Function templates are pervasive in the Standard C++ Library. The extensive use of type deduction and default arguments makes their use almost transparent to the ordinary programmer. The 80+ items in the algorithm part of the library depend on function-template technology, but you can write a great deal of C++ without realizing that it is the magic of templates that is making your code work.

GENERIC FUNCTIONS

141

Language Note: Readers with a background in dynamic and scripting languages may be surprised that I consider templates to be anything special. Many scripting and dynamically bound languages such as Perl and Python have a good deal of genericity built in. What C++ offers is the ability to write extensive code in a type-independent way. C++ was the first mainstream language to offer this facility. It has resulted in a great deal of innovation. Some of this has stretched the syntax of templates in C++ to breaking point. Even Bjarne Stroustrup, the original designer of C++, has been surprised by some of the things that templates have achieved.

C H A P T E R 8

User-Defined Types,

Part 1: typedef and enum

C++ provides a number of mechanisms for declaring and defining new types and new type names. These range from a simple mechanism (typedef) for declaring a name to be an alternative name for an existing type to mechanisms for defining entirely new types and the ways that the existing C++ operators will work with those types. In this and the next couple of chapters, I will be introducing you to these mechanisms, showing you how you have already been using them and how you can provide your own type names and types.

typedef: New Names for Old

The typedef keyword allows us to provide a new name for an existing type. That is all it does. The new name is a pure synonym for the old one. There are three primary reasons for wanting a new name for an existing type: opaqueness (wanting to hide what type is being used); using a more descriptive name (i.e. avoiding ‘magic’ types); and simplification (reducing types with several parts to their name to a single name).

I will now deal with each of these three uses of typedef declarations.

Opaque Type Names

Sometimes we want to be able to change the actual type used without having to modify large parts of our code. We have already had examples of that in the code earlier in this book. The Library makes extensive use of typedef for this purpose. For example, you will frequently come across size t as the name of a type; in general, we do not need to know the exact type for which it is a synonym. The C++ Standard requires that it is one of the unsigned integer types. The exact type must be suitable for representing the count in bytes (unsigned chars) of any object that the implementation will support. The size of the largest possible object is implementation-defined. unsigned long is the most common underlying type for size t, though some systems choose unsigned int. We will not have any problems as long as we use size t in the way that the designers of C++ (and C) intend. However, we should note that size t is intended for representing the amount of storage allocated for an object. Nothing in the language will prevent us from using it for other things, and, as far as the compiler is concerned, it is just another way of writing whatever the typedef provided by the implementation has declared it to mean.

When the implementer of the Library puts

typedef unsigned int size_t;

144

CHAPTER 8

in a header file, the result is that size t is just another name for unsigned int wherever it is used in code being compiled by the implementation. If you move your source code to another compiler with

typedef unsigned long size\_t;

in the header file, uses of size t in your code will now be treated as meaning unsigned long. It is your responsibility as a programmer to ensure that your code does not depend on the precise type. As long as you use size t in the intended way, there will be no problems.

time t and clock t are two other opaque types from the C++ Standard. They are specifically for dealing with certain aspects of measuring time. They can be any fundamental arithmetic type – integral or floating point. The wide range of possible types that those type names can alias makes it particularly important that the programmer only use the names for the exact purpose for which they were intended. Careless or ignorant usage can result in surprises when porting code between implementations. This is a general problem with using typedef to create an opaque type name: it places a burden on the programmer to avoid abuse.

In the last chapter, we had another variant of the idea of an opaque type. If you look back, you will see that I used a typedef to provide a single place where I could change a type used in a program. The introduction of generic programming tools into C++ has made that use much rarer. We use templates if we want to reuse the same basic code with only a type change.

Descriptive Type Names

Look at this small program:

#include <iostream> #include <istream> #include <ostream>

int main( ){

std::cout << "How old are you? "; int age;

std::cin >> age;

std::cout << "In five years you will be " << age + 5 << " years old.\n";

}

Why is int the type of age? Think about that, because there are quite a few hidden assumptions floating around. For example, I never said that I wanted the user’s age as an integer value. Yes, most adults would give their age in years, but children will not always do so.

Just as experienced programmers avoid using magic numbers they also avoid using magic type names. Their reasons are much the same; they want to write code that is more self-documenting, or they want to avoid repetitive writing of complicated expressions. We prefer pi to 3.14159265 because the first is simpler and far less prone to error. We also prefer it because it makes it clear that we are using a specific mathematical constant rather than some arbitrary decimal value.

We do not gain very much from writing:

typedef int years; years age;

Indeed, in a program as brief as the one above we gain nothing of value, but there are cases that are more complicated where there can be some benefit.

USER-DEFINED TYPES, PART 1: typedef AND enum

145

Dealing with Complicated Type Names

The names of many types used in C++ are composed of several tokens. For example, unsigned int const * is a four-token name for a type. There are several problems with multi-token names: they are often hard to read; given two instances, it takes time to check that they are the same type; and worst of all, C++ often allows the tokens to be reordered. In the example I have just given, all six possible orderings of unsigned, int, and const are allowed and are equivalent. Using a typedef name alleviates most of those problems.

There are also cases where a type name is just plain complicated. For example, C++ inherits the C library, and in that there is a function called qsort( ). qsort( ) is intended for sorting arrays. We do not make much use of qsort( ) in C++ because, as we have seen, C++ has a more powerful set of generic tools provided by the Library. However, it can be useful to know about such pure C functions when writing code to work in a mixed C and C++ context. The problem is that qsort( ) needs to know what function it can use to compare members of the array. That information is provided by a function pointer passed as an argument (we will learn about function pointers in a later chapter after we have spent time on pointers). However, what is the type of the parameter that will receive that function pointer? If I told you that it was int (*)(void const *, void const *), I doubt that you would be much the wiser. Such a type name is both hard to remember and useless for documentation purposes, even if you are fluent with C++ declarations. If I write the correct typedef, I can replace the declaration of qsort( ) as

void qsort(void * base, size_t elements, size_t size, int (*compare)(void const *, void const *));

(which says that qsort needs four arguments: where the array starts, how many elements there are in the array, how big each element is, and something to compare them with) with:

void qsort(void * base, size_t elements, size_t size, compare_function_t compare);

The type of the fourth parameter in the first case is pure magic, but the second form is, I believe, much more helpful to the reader. Of course, if you need to know the details of the type of a comparison function you will have to look at the typedef, which is:

typedef int (* compare_function_t)(void const *, void const *);

However, you only consider that declaration when you need to, and do not have to unpick the complicated type name the rest of the time. Unpicking complicated declarations takes time and experience, so do not worry about how the above works

We will come back to typedefs in a later chapter, but for now you know all you need to know about them.

On Reading Declarations

C++ shares an overly complicated declaration syntax with C. Bjarne Stroustrup, the original designer of C++, has described it as an interesting experiment that failed. C++ retained the C declaration syntax for backward-compatibility with C. The cost of that compatibility is that every C++ programmer has to learn how to read these declarations, as well as how to write them when the need arises.

The secret to reading a declaration is to determine what name is being declared. This can sometimes be less than simple. However, the first step is to recognize which statements are declarations. A declaration usually starts with a type name, a type modifier (const or volatile) or a storage-class specifier (extern, auto, static, or typedef). C++ also has some special cases such as the declaration of a type name with class,

146

CHAPTER 8

struct, union, or enum. However, those special cases are not generally part of complicated declarations, so we do not need to consider them here.

Once you have decided that you are looking at a declaration, the next step is to determine what name is being declared. Look for the first thing that is neither a keyword nor the name of a type. Once you have found the principal name being declared, look to the right until you find an unmatched closing parenthesis. Read everything between the name and that parenthesis (we will see how in a moment). Next move to the left of the name until you find an opening parenthesis and interpret everything between the name and that parenthesis, reading from right to left. Now repeat that process by first looking to the right of the closing parenthesis found earlier and then to the left of the opening parenthesis just found. Repeat that process as often as necessary.

Here are a few examples:

int const * volatile ivc;

There is nothing to the right of ivc, so traverse the tokens to its left in right-to-left order. That gives us: ‘‘ivc is a volatile pointer to a const int.’’

double * const data[5];

The name being declared must be data. Immediately to its right is a pair of square brackets. That is read as ‘array of ’. The 5 tells us that the array has five elements. We have run out of items to the right of data, so we now read from the left of it (in a right-to-left direction) giving const pointers to double. Therefore, the whole declares that data is an array of five const pointers to doubles.

double (* const data)[5];

Now I have inserted a pair of parentheses, which will change what we are declaring. There is a closing parenthesis directly after data, so we must first read what is on its left as far back as the opening parenthesis before reading the [5] and concluding with the double. That gives us: ‘‘data is a const pointer to an array of five doubles.’’

If while we are searching to the right we come to an opening parenthesis, that will be the function operator, and the material from there to the corresponding closing parenthesis will be a parameter list.

double & (* data)(int, double, void (*)(int));

When we unpick that, we get: ‘‘data is a pointer (that is the (* data) part) to a function with three parameters: int, double, and a pointer to a function with one parameter of type int that returns a void. (That last parameter is the void (*)(int).) The function pointed to by data returns a reference to a double (that is the initial double &).’’

That last example is ugly. We can get even uglier if we want to declare a name for an array of pointers to functions. Fortunately, we do not need these things for most of our programming. When we do, a judicious use of typedef to create readable names for bits of the declaration makes it much easier both to write and to read complicated declarations. For example, if I need to declare an array of 10 pointers to functions taking an int and returning a double, I start by declaring a type name for the function pointers:

typedef double (* func_ptr)(int);

This declares func ptr to be a pointer to a function that has an int parameter and returns a double. Now I can use that as a stepping stone for the declaration of the array:

func_ptr data[10];

This declares data to be an array of 10 func ptrs. In other words, data is an array of 10 pointers to functions that have an int parameter and return a double. It is usually better to break down complicated declarations by careful uses of typedef.

USER-DEFINED TYPES, PART 1: typedef AND enum

147

enum

C++ has inherited a curious form of user-defined type from C. These types are created by using the keyword enum. If you are a C programmer, you need to be careful, because C++ has modified the rules. You should not assume that you know all the details just because you are confident of the use of enum in C.

We use enum to create a user-defined type that is restricted to integral values, and to provide (usually) one or more named enumerated values. Those named enumerators are part of the definition of an enum type – we cannot add them later. Any value of any enum type implicitly converts to an integer value. However, there is no implicit conversion in the other direction.

Suppose that I want to write a program that is concerned with different types of cloth. I might want to categorize the cloth by the type of yarn used in its manufacture. I could create a type to represent the yarns with:

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

That definition declares yarn as a type name for an integer type with the five named values (or enumeration constants) cotton, linen, silk, nylon, and other.

I have said that an enum is a type with only integer values, so it makes sense to ask what the values of the enumeration constants will be. C++ provides some simple rules:

1.The value can be provided by explicitly ‘assigning’ it within the definition.

2.If there is no provided value for the first enumeration constant it takes the default value of zero.

3.Any other enumeration constant that is not explicitly assigned a value implicitly takes the value of the immediately preceding enumeration constant plus one.

4.Two or more enumeration constants can share a value.

When we apply those rules to the above case, we get that cotton is 0, linen is 1, silk is 2, nylon is 3, and other is 4. Do not just take my word for it, but try this little program to check that my assertion is correct (yes, please do so, because we will be adding things to this program over the next few paragraphs).

#include <iostream> #include <ostream>

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

int main( ){

std::cout << "cotton is " << cotton << '\n'; std::cout << "linen is " << linen << '\n'; std::cout << "silk is " << silk << '\n'; std::cout << "nylon is " << nylon << '\n'; std::cout << "other is " << other << '\n';

}

At this stage it might not seem very surprising that the compiler accepts this program. However, we have just asked std::cout to output five values of a type of which it had no prior knowledge. Behind the scenes, the compiler hunted for some way to fulfill your request. What it found was that it was allowed to convert a value of an enum type into the value of an int. It knows how to make std::cout handle int values, so the compiler went ahead and carried out the implicit conversion from yarn to int.

Now modify the definition of yarn to

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