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

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

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

98

CHAPTER 5

13.Write a definition for:

int get_int(std::istream data_source, int number_of_retries);

14.Write an overloaded set of functions to get a double from a std::istream object, from std::cin both with and without a prompt, and in each case with and without a specified number of retries. How are these different from the set of overloaded functions for getting an int value?

Default Arguments

Consider the following function declaration:

void plot_square(fgw::playpen &, int x, int y, int size, fgw::hue);

This function always requires that the caller explicitly provide a color as an argument for the last parameter. In some circumstances, you might find it reasonable to allow the color to default to black. We can do that with a forwarding function that overloads the above declaration:

inline void plot_square(fgw::playpen & paper, int x, int y, int size){ return plot_square(paper, x, y, size, fgw::black);

}

Language Note: C does not allow programmers to return a void. C++ specifically allows a return of a void so that programmers can use a single consistent idiom for forwarding functions such as the above. Without that provision, we would have to omit the return and just write a call to the function to which we are delegating the work. This might not seem to be a great burden, but when we cover writing generic functions in a later chapter, we will see that distinguishing a void return type from all other types is an irritant or worse.

Notice that the forwarding function is provided as a definition. This is the normal way to do that but it has two costs. First, we probably want to make that definition available to the compiler wherever we call the function, so that it can replace the call with a call to the function to which we are delegating the work. To allow such potential multiple definitions we need to qualify the function as inline. The fact that that also advises the compiler to replace the actual call with a call to the delegated function is not important – good compilers will do that anyway. What is important is that inline overrides redefinition issues.

The second point is that I have to name all the parameters. In the earlier pure declaration, I could skip naming the first parameter because there is no documentary advantage (well I do not think there is). I do not have the same freedom when it comes to a declaration that is also a definition.

One disadvantage of using a forwarding function is that it may not be immediately clear what is being provided and why. At the very least, the definition should be preceded by a suitable comment that explains the what and the why. However, C++ provides another option if the default (special-case) value is the argument for the final parameter. The method is to provide a default argument. Using a default argument we could rewrite the original declaration of plot square( ) as:

void plot_square(fgw::playpen &, int x, int y, int size, fgw::hue = fgw::black);

When we do that, we tell the compiler that if the function is called without an explicitly provided final argument, it is to use the one provided in the declaration. Default arguments must be provided in declarations:

WRITING FUNCTIONS IN C++

99

the compiler needs to know what the defaults are so that it can add them to a call when the programmer omits one or more of them.

We are not limited to using a default for the final argument. However, we cannot use defaults for earlier arguments unless we also use defaults for all the subsequent ones. Once we start relying on a default argument for a function call we must use the defaults for all the remaining arguments. For example, we could declare plot square( ) as:

void plot_square(fgw::playpen & = paper, int x = 0, int y = 0, int size = 1, fgw::hue = fgw::black);

(Note that default arguments are always provided by the syntax = default, which comes after the parameter name if one is provided.)

If I provided this declaration and then wrote the statement

plot_square( );

in my source code the compiler would behave as if I had actually written:

plot_square(paper, 0, 0, 1, fgw::black);

In this case providing defaults for all the arguments is unlikely to be useful. As C++ does not allow me to write

plot_square(, 10, 10, , fgw::black);

as shorthand for:

plot_square(paper, 10, 10, 1, fgw::black);

there is little value in this case to having defaults for all the parameters.

The problem with using default arguments is that once we start using defaults we are stuck with them for all the remaining parameters. For example, suppose that I want the size of the square to default to the currently used scale. I could do that by making size default to 0, because the way the scale feature of Playpen is designed is to ignore out-of-range scales. However, which is more likely? That I want to use black? Or that I want to use the current scale? The way I have designed the plot square( ) function, I can only make the color default to black, or make the size default to the current scale and the color to black. I cannot specify the color but use the default value for the size.

This is a limitation to default arguments, which makes them less powerful than forwarding functions. For example, I can write

inline void plot_square(fgw::playpen & paper, int x, int y, fgw::hue shade = fgw::black){

return plot_square(paper, x, y, 0, shade);

}

to provide a version of plot square that uses the current scale as the size of the plotted square.

Example of an Overloaded Set of Functions

Here I bring together the ideas we have explored in the preceding section so that we can focus on their use and possible consequences. Consider these three function declarations:

100

CHAPTER 5

void plot_square(fgw::playpen &, int x, int y, int size, fgw::hue = fgw::black);

inline void plot_square(fgw::playpen & paper, int x, int y, fgw::hue shade = fgw::black){

return plot_square(paper, x, y, 0, shade);

}

inline void plot_square(fgw::playpen & paper, int size, fgw::hue shade = fgw::black){

return plot_square(paper, 0, 0, size, shade);

}

The first thing we must do is check that all the combinations of using default arguments and overloaded alternatives result in distinguishable calls. In other words, the compiler must be able to tell from the call which of the overloaded functions to use and which default arguments will complete the chosen call. I find that the most useful way to do this check is to list all the variations of the type lists. Here they are for the above set of overloaded functions:

Using the first declaration with all five arguments:

void plot_square(fgw::playpen &, int, int, int, fgw::hue);

Using the first declaration with the default fgw::hue:

void plot_square(fgw::playpen &, int, int, int);

Using the second declaration with all four arguments:

void plot_square(fgw::playpen &, int, int, fgw::hue);

Using the second declaration with the default fgw::hue: void plot_square(fgw::playpen &, int, int);

Using the third declaration with all three arguments:

void plot_square(fgw::playpen &, int, fgw::hue);

Using the third declaration with the default fgw::hue:

void plot_square(fgw::playpen &, int);

Check this list carefully. Every one of the possible sets of explicitly provided argument types is different. That means the compiler can distinguish the various choices as long as we provide exactly the right types of arguments.

We are not quite finished. What if we do not provide exactly the correct types? For example, suppose we write:

plot_square(paper, 1.12, 1.4, 13)

WRITING FUNCTIONS IN C++

101

Now the compiler will go through a routine looking for a best match. Effectively it creates a list of candidates such as the one above. It discards any that have the wrong number of parameters. In this case, it is left with just two possibilities:

void plot_square(fgw::playpen &, int, int, int); // default fgw::hue void plot_square(fgw::playpen &, int, int, fgw::hue); // first inline

Next, it takes each argument in turn and categorizes it as an exact match, or some level of mismatch all the way up to ‘‘no implicit conversion makes the argument match the parameter type’’. In this case, the first argument is an exact match for both candidates. The second and third arguments can be converted to int by the standard conversion of double to int. That applies for both the candidates. Finally, the last argument (13) is an exact match for an int but requires a conversion to make it a value for an fgw::hue. (That is one reason that I made the color codes a user-defined type in my Playpen library instead of lazily using an int. I wanted to be able to distinguish between colors and ints even though colors were coded with integer values.)

The first of the two candidates is as good as the second for three of the arguments and better for the fourth. That allows the compiler to make an unambiguous choice.

Do not worry too much about ‘best match’ problems when using overloaded functions, possibly combined with default arguments. The compiler will make a choice, tell you that there is no viable choice, or tell you that there is no single best choice. Later we will see how to resolve the last of these.

However, when overloading functions you should be careful to ensure that all the overloads conceptually do the same thing. Do not overload a draw( ) function so that when the argument passed to it is a gun type the result will be a shooting whilst an argument of a pencil type results in a sketch.

Enough theory. Time for some more practical work. Here is a first (faulty) definition of the general plot square( ) function:

void plot_square(fgw::playpen & paper, int x, int y, int size, fgw::hue shade){

paper.scale(size); paper.plot(x, y, shade);

}

Note that a definition does not generally include declarations of default arguments unless it is also doubling up as a function declaration.

Here is a small program to test that function definition:

int main( ){ try{

fgw::playpen paper;

plot_square(paper, 5, 5, 16, fgw::red1); paper.display( );

std::cin.get( );

}

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

}

T R Y T H I S

Create a new project for a Playpen-based program. Create a source-code file. Add a suitable opening comment. Add the necessary headers. Type in the declaration of the general plot square( )

function followed by the inline definitions of the two specialized overloads. Now type in the test program followed by the definition of the general plot square( ) function. Compile and run the result.

102

CHAPTER 5

T R Y T H I S

Go back to the previous task and modify the test program so that it tests all six possible ways of calling plot square( ). Now try changing some of the arguments so that they do not exactly match the parameter types. Some changes may result in code that will not compile; some may result in the compiler issuing a warning (it can do what you asked, but it wants to tell you that what you provided was not exactly what it expected). Try to get a feel for what is happening.

Something Is Not Right

Perhaps you have noticed that calls to plot square( ) have a side effect. Not only do they plot a square, but they also change the scale of subsequent uses of the fgw::playpen object. Fortunately, we can fix this problem, because the fgw::playpen type has a function that tells you what the current scale is. It is an overload of fgw::playpen’s scale( ) function. It is also an exception to the guideline that overloads should conceptually do the same thing. If I write paper.scale( ) I will get an int value returned that represents the current scale of paper. In other words, if I do not provide a value for scale( ), the call tells me what the current value is, but if I do provide a value it changes the scale to the value provided.

I can use this to ensure that plot square( ) does not change the scale of the fgw::playpen object it is using, or more correctly, that it restores the previous scale before it finishes. Here is the corrected version:

void plot_square(fgw::playpen & paper, int x, int y, int size, fgw::hue shade){

int const old_scale(paper.scale( )); // save prior scale paper.scale(size);

paper.plot(x, y, shade);

paper.scale(old_scale); // restore to prior scale

}

I only have to make that change in a single place because the overloaded, specialized versions use the general version to do the real work. It is important to get into the habit of saving any state that you are going to change temporarily and restoring it at the end.

This example should also have emphasized the value of reusing code by calling a single function that does the real work from specialized overloads.

T R Y T H I S

Test your code from the previous task to check that it works with the corrected version of plot square( ).

EXERCISES

Some of the following will require you to reuse earlier functions such as get int( ). In other cases, you should consider writing auxiliary functions that might be used in future exercises. Many of these functions may be very

WRITING FUNCTIONS IN C++

103

short. Do not let that deter you from writing them. Well-named short functions can often enhance your code by making it more readable. Modern compilers will usually produce just as effective a program from such code.

15.Write a program that repetitively prompts the user for the arguments for a call to draw square( ) and then uses them to draw a square in the Playpen window. The program should repeat the process until the user signifies that they want to stop. Part of this exercise is for you to decide how the user will signify the end of the program.

16.The fgw::playpen function setplotmode( ) handles the problem of restoring a previous state differently from the way that the overloaded scale( ) functions do. setplotmode( ) returns a value of type fgw::plotmode, which represents the previous plotting mode. So, for example,

fgw::plotmode old(pad.setplotmode(fgw::disjoint));

simultaneously sets pad’s plotting mode to fgw::disjoin and saves the previous state in old. When you have finished you can restore the previous plotting mode with:

pad.setplotmode(old);

Write and test a definition for:

void plot_square(fgw::playpen & paper, fgw::plotmode pm, int x, int y, int size, fgw::hue shade);

If you think carefully about the ideas behind forwarding functions, you should be able to do this with very little extra code by delegating most of the extra work to the already written general plot square( ) function. This is an example of using forwarding functions to extend a generalization.

17.Overload plot square( ) so that for each of the original ways that we could plot a square there is a corresponding version to plot a square in a selected plotting mode. Write a program to test all 12 possible ways of calling plot square( ).

18.Extend Exercise 15 to include selection of the plotting mode. Note that this is not a trivial extension, because you are going to have to decide how to obtain the choice of plotting mode from the user of the program.

Unnamed Parameters

There is one final feature of functions in C++, and that is an allowance for parameters to remain unused in a function’s definition. It may seem odd that we would ever want to write a function that will be provided with an argument that is not used. You will have to wait some time before I can give you a good example of the utility of this feature, but this chapter on functions would not be complete if I did not mention unnamed parameters.

Good compilers warn you about variables and parameters that do not seem to have been used in the source code. For example,

int foo( ){ int i(10); return 1;

}

looks to be deeply suspicious. Did the programmer really intend not to use i? Such warnings are helpful and we do not want to turn them off, because more often than not, the compiler is correct to be suspicious, and the warning helps us to debug our code. However, in the case of parameters there are times when we genuinely do not want to use the argument provided by the function call.

104

CHAPTER 5

Suppose you are writing a function to open a window and you want it to be portable across several operating systems. That almost certainly means that you will need to provide a different definition of the function for each of the systems. The declarations will be the same regardless, but we need to provide definitions tailored to specific systems. Now imagine that one system allows you to place a colored frame around the window, but another does not. That means that the function declaration must allow for data about the color of the frame. The system that does not provide colored frames cannot use that information and will simply have to ignore it.

To avoid the warning for an unused parameter, C++ allows the programmer to omit a name for any parameter that is unused in the definition. This omission is in the definition of the function (parameter names may always be omitted in function declarations), and its only effect is to suppress the warnings that most compilers would otherwise generate.

In high-quality development environments, where code is required to compile warning-free even at the severest warning level, this is a greatly appreciated feature of C++.

Separate Compilation and Header Files

Until now, I have asked you to place all your source code in a single file. That is not generally good C++ coding practice. We want to be able to reuse code as well as reduce build times.

Programmers coming from such languages as Java find the idea of separate compilation strange, because they are used to a somewhat different strategy wherein libraries are loaded at execution time. They actually do have a kind of separate compilation but not the kind that C++ uses.

C++ (like C) packages declarations and other items (such as inline definitions) into special source-code files called header files. The concept of a header file is to provide the compiler with exactly the information it needs to use functions, types, and other material whose definitions will be provided either by another file of source code or by a library. We limit the compiler’s knowledge to what it needs in order to do its job, and postpone the rest of the process of producing a program to two other tools: a linker and a loader. The job of the linker is to combine code from different places into a single executable. The job of the loader is to fix up a specific execution of a program by providing suitable resources and ensuring that the executable knows the locations of those resources.

All those header files you have been including into your programs are files of declarations and other material such as some inline definitions that the compiler needs to compile your source code. When you then build your executable, the linker takes all the compiled code nominated by your project together with the general C++ libraries, some system libraries, and any special libraries specified in the project (for example, the gdi32 and fgw libraries you have to add to your project when using fgw::playpen objects).

When you use #include in your source code, you are telling the compiler where to look for supplementary information it may need. When you add libraries to your project, you are telling the linker where to find supplementary object code (the result of compiling source code) that will be needed to produce a complete program.

It is not only library writers who can limit the exposure of their work at compile time; you can as well. Indeed, you are encouraged to do so.

In order to use separate compilation you need to separate your code into distinct though meaningful files. One part of this separation is to create pairs of header and implementation files. To see how this works carry out the following task.

T R Y T H I S

Create a new project in MDS. Now create a header file called plot square.h. Note that creating a header file is one of the options for creating new files. Now type the following into that header file

WRITING FUNCTIONS IN C++

105

(mostly you can cut and paste from your earlier use of the overloaded plot square( ) functions):

#include "playpen.h"

// declarations

void plot_square(fgw::playpen &, int x, int y, int size, fgw::hue = fgw::black);

// inline forwarding functions

inline void plot_square(fgw::playpen & paper, int x, int y, fgw::hue shade = fgw::black){

return plot_square(paper, x, y, 0, shade);

}

inline void plot_square(fgw::playpen & paper, int size, fgw::hue shade = fgw::black){

return plot_square(paper, 0, 0, size, shade);

}

The first line is because the subsequent code needs to know the declarations of such things as fgw::playpen and fgw::black. Header files, particularly user-written ones such as this one, often include other header files. Those included header files provide information for the compiler, but they also provide information for the programmer: the list of inclusions tells the programmer about the way this header depends on other facilities. In this case, we can see that the rest of the file only depends on fundamental C++ and facilities provided by playpen.

Now go back to your test program. Remove the above material from it and replace it with #include "plot square.h". Next cut out the definition of the general plot square( ) function and paste it into a new C++ source file called plot square.cpp (note that the IDE adds the correct extensions for you, as it did for the header file). Add #include "plot square.h" to the beginning of this file. (This is not strictly necessary in this case – #include "playpen.h" would have been enough. However, later we will need to include headers into implementation files, so there is no harm in developing good habits now.)

Finally add this source-code file to the project, along with the modified source-code file with the version of main( ) that tests the code. The latter file now contains just the necessary #includes and the definition of main( ). Everything else has been separated out into a header file, containing the material the compiler needs to compile the file with main( ) in it, and an implementation file, which, when compiled, provides the linker with the material it needs in order to complete the executable.

EXERCISES

19.Rework Exercise 18 so that all your function declarations are in header files and their definitions are in implementation files. The material for the overloaded plot square( ) functions should be in one pair of header file and implementation file. Any other functions that you wrote to assist with the program (and there should be several) should be in a distinct header/implementation file pair. Learn to keep separate material in separate files.

20.Extract all the declarations and inline definitions for get int( ) and get double( ) into a suitably named header file. Extract the non-inline definitions into an implementation file. Now create a project and write a program that tests all the variations of your get int( ) and get double( ) overload sets.

106

CHAPTER 5

REFERENCE SECTION

Function Declaration Syntax

All functions are declared (i.e. their names are provided to the compiler) with the following syntax: return-type function-name(comma-separated-parameter-type-list);

The return type is almost always required (we will find that there are a couple of exceptions for some special member functions). The use of the special void type signifies that there is no return value and so the function is effectively a procedure.

The function name has the same restrictions as all other names in C++ and must be present. The parameter type list is a (possibly empty) list of types. Each of the declared parameters may

be followed be a name. The parameter names in pure declarations (ones that are not also definitions) have no significance anywhere else; they merely serve to document the purpose of the parameters in question.

A semicolon terminates a function declaration unless it is also a function definition. In that case the body of the definition terminates the declaration part of the definition.

namespace my_library {

double sales_tax(double cost, double percentage_sales_tax_rate);

}

Function Definition Syntax

The syntax for a function definition is very similar to that for a function declaration except that:

The body of the definition, which consists of zero or more statements enclosed in a pair of braces, replaces the terminal semicolon of the pure declaration. If the function has a return type other than void those statements must include a return statement providing a suitable value or object that will be returned to the point where the function is called.

Each parameter that is used in the definition body must be named (the name is how the object or value provided by the caller of the function is used in the body of the function).

One or more namespace or class names followed by scope operators may precede the function name.

double my_library::sales_tax(double c, double rate){ return c * rate / 100;

}

namespace my_library {

double sales_tax(double c, double rate){ return c * rate / 100;

}

}

Function Call Syntax

A free function is called by using the function name (possibly fully elaborated with namespace names) followed by a comma-separated list of arguments in parentheses. The parameters provided in the definition are initialized with the arguments provided by the call.

WRITING FUNCTIONS IN C++

107

A member function (dealt with in Chapter 9) is called using the dot (.) operator, with its left operand being the name of a class object and its right operand being the function name; a comma-separated list of arguments in parentheses follows. A member function may also be called by using a pointer to an object of the class type. In that case the arrow (->) operator takes the pointer as its left operand and the function name as its right operand; again a comma-separated list of arguments in parentheses follows.

double tax(0);

tax = my_library::sales_tax(27.50, 6.25);

The above (free) function call might be used to calculate a 6.25% sales tax on an item costing 27.50 monetary units (dollars, pounds, euros, etc.).

fgw::playpen paper; paper.plot(12, 23, red1);

The above (member) function call plots a dim red pixel at the coordinates (12, 23) in the Playpen window that is the output device used by the fgw::playpen object named paper.

Function Overloading

Functions declared in the same scope may have the same name as long as they have distinct parameter type lists. The difference in the parameter types must be sufficient for the compiler to select one solely based on the types of the arguments provided by a function call using the name. Differences in return type are not significant for selecting which of an overloaded set of functions is used.

If there is a function whose parameter types exactly match the types of the arguments in the function call, it will be selected. If there is no such function, the compiler will invoke a number of ‘best match’ rules. The details of these are complicated. However, in general, if it matters which function is called then the overload set is poorly designed. The intention of providing overloaded function names is to allow programmers to achieve essentially the same objective from possibly different types of data.

One of the more useful forms of function overloading is to deal with the special case where one of the arguments is implicit. The special case can be defined by forwarding the explicit arguments to the general version with the implicit argument or arguments added in.

Given a function whose declaration is

void drawline(fgw::playpen &, point start, point end, fgw::hue);

(with point being some type that represents a point on a plane), the following defines an overload that draws black lines:

void drawline(fgw::playpen & p, point start, point end){ return drawline(p, start, end, fgw::black);

}

This one would draw a line from the origin (assuming that the correct way to create a point object identifying the origin is point(0, 0)):

void drawline(fgw::playpen & p, point end, fgw::hue shade){ return drawline(p, point(0, 0), end, shade);

}