• Aucun résultat trouvé

Multi-way choices with switch

Dans le document You Can Do It! (Page 167-172)

In order to define the third function I need another C++structure,switch. Several decades of programming experience has convinced expert programmers that having lots of conditional expressions (such as those provided byif) often leads to contorted code that is difficult to follow. Such code is easily written

incorrectly and is a cause of bugs in programs. C++has a special way to handle the case where we want to choose one of several options. It only applies to cases where the control variable is a whole number. In this context the numerical codes that representcharvalues are treated as whole numbers.

We need four new keywords. To make it easier to follow I am going to write a short C++program and then explain what each of the keywords does. I have highlighted the new keywords.

#include "fgw_text.h"

#include <iostream>

using namespace std;

using namespace fgw;

int main(){

int const value(read<int>("type a number between 0 & 4 exclusive: "));

switch(value){

case 0:

cout << "0 is outside the range.";

break; case 1:

case 2:

case 3:

cout << "the square of "<< value << " is " << value*value;

break; case 4:

cout << "4 is not included in the range.";

break; default:

cout << value << " is not acceptable.";

} }

switch(value)starts the process. It tells the compiler what number will be used to select an action.

You can put expressions inside the parentheses as long as they will result in a whole number when the program runs.

Thecasekeyword must be followed by an integer constant. By that I mean a whole number that is visible to the compiler. We cannot use variables after acasebecause the compiler would not know the value they stored but we can use named numbers such as those defined as having typeint constand initialized with a literal. The mechanism I use to initialize anint constfromread<int>will not do because while that gives an immutable number it is one provided at the time the program runs which is after the compiler has finished its work.

When the program runs it will look for acase-value that matches the value in theswitch()and then execute the code that follows the colon. It will continue until something stops it. That is the job of thebreak

keyword in the above code.breakcauses the program to jump to the end of theswitch-block (the closing brace that corresponds to the opening brace afterswitch(value)).

Notice that we do not have to have abreakfor everycase. If, as in the above, there are several values that we want to treat the same way, we can provide a list ofcases and provide the shared action at the end of the list.

The fourth keyword,default, is to allow us to provide a blanket option for all the other possible values of the control variable that we do not deal with on an individual basis.

If you need to make a multi-way choice based on a whole number value then theswitch-statement is almost certainly the way to go. However always think about whether there is a simpler way to develop the code. Some people get lazy and use decision statements such asswitchwhen there are better options. One of the problems withswitchis that you must know all the choices at the time you write the source code. That is not always possible.

‘‘ R O B E R T A ’’

You say that you must know all the choices when you write the code but in the next chapter you demonstrate that you can add more later. So can you elaborate please?

F R A N C I S

You can add more later on but only by editing your source code. In other words you have to recompile the code. There are more advanced mechanisms thanswitchthat allow retro-fitting choices without recompiling code. Those are beyond the level of this book.

We now have all that we need to provide a reasonable definition of our third function, the one that acts on the choice that the user has provided viaget_choice(). Here is my code:

bool do_choice(int choice){

bool more(true);

switch(choice){

case 0:

clear_playpen(???);

break;

case 1:

more = false;

break;

case 2:

change_plotmode(???);

break;

case 3:

plot_pixel(???);

break;

case 4:

change_scale(???);

break;

default:

throw problem("Not a valid option");

}

return more;

}

The values used in theswitchstatement are provided by the order of the letters in thechoicesstring, which happens to be in alphabetical order rather than the order of the menu entries. This is another example of magic values. We will eventually remove the magic values with the result that the code will be easier to follow.

Notice that in most cases I delay the action by delegating to a function that I have yet to write. That is generally good programming practice. The only case that I can deal with immediately is the one that handles finishing. By the way, do those magic uses of 0, 1, 2, 3 and 4 worry you? I think they should but we will deal with that shortly.

What about those question marks? I had completely forgotten that the various actions would need to know whatplaypenobject was being used. Careless, but because I have kept each step simple, it will be easy to go back and correct the problem.do_choiceneeds an extra parameter to track theplaypenobject so that it can pass it on to the functions that need to use the Playpen to do their work.

I need to go back to the declaration ofdo_choiceand change it to:

do_choice(fgw::playpen &, int action);

I will also need to go back to the test program and correct that to:

int main(){

playpen picture;

bool more(true);

do{

display_choices();

int const choice(get_choice());

more = do_choice(picture, choice);

} while(more) }

And finally correct and complete the definition ofdo_choice():

bool do_choice(playpen & pp, int choice){

bool more(true);

switch(choice){

case 0:

clear_playpen(pp);

break;

case 1:

more = false;

break;

case 2:

change_plotmode(pp);

break;

case 3:

plot_pixel(pp);

break;

case 4:

change_scale(pp);

break;

default: throw problem("Not a valid option");

}

pp.display();

return more;

}

We still have work to do because we have to provide the four functions that carry out the actions. I am going to work on a need to know basis. The only part of my program that needs to know about these four functions is the definition ofdo_choice. That suggests to me that I should hide these functions away in the same file that contains the definition ofdo_choice. If I later decide that the functions are of more general use I can publish their existence by placing suitable declarations in a header file. In the meantime, I want to restrict the functions to the file where they are defined.

C++provides a mechanism to do that. You place the declarations and definitions (usually just an early definition which will also be a declaration) in anunnamed namespace(that is simply a namespace without a name). Anything declared in an unnamed namespace can be used freely within the file but cannot be used anywhere else. This is a useful technique where you want functions, types, etc. that are strictly local to a file.

This is another form of information hiding. Good programming tries to restrict information on a need to know basis. The less outsiders know, the less room they have for making mistakes.

Here are (incomplete in some cases) definitions of the four functions:

namespace { // open an unnamed namespace void clear_playpen(playpen & pp){

hue const new_hue(read<int>("What background color? "));

pp.clear(new_hue);

}

void change_scale(playpen & pp){

int const new_scale(read<int>("What is the new scale? "));

// for you to finish }

void change_plotmode(playpen & pp){

// write an implementation that gives the user a // menu of the four options and asks them to choose // then use a switch to apply the choice

// if you are not ready to do this just leave in the following // line:

cout << "***not implemented yet.***\n";

}

void plot_pixel(playpen & pp){

int const x(read<int>("What x value? "));

int const y(read<int>("What y value? "));

hue const shade(read<hue>("What color? "));

pp.plot(x, y, shade);

} }

Note that the unnamed namespace must be closed (have the terminating right brace) before you define anything that is to be available elsewhere. You can reopen the unnamed namespace later on if you wish. Do not worry that you cannot qualify names declared in the unnamed namespace, the compiler treats all those names as if they had been declared in the enclosing namespace but only in the file where you placed the unnamed namespace. An unnamed namespace in a different file will be a different namespace and not an extension of an existing one.

If you later decide you want to use a function in another file, you just add its declaration to a suitable header file then cut the definition and paste it outside the unnamed namespace. Some programmers advocate that all functions and global variables (those outside any functions, classes, etc.) should be placed in an unnamed namespace until you know you want to use them elsewhere. As soon as you place declarations in a header file you implicitly use them elsewhere and so the definitions of those functions must not be placed in an unnamed namespace.

TASK 19

Complete the definitions forchange_scale()andplot_pixel().

When you complete the above and add them to theart_menu.cppfile you should be able to build the Menu project and test it. You will need to make sure you add those functions before the place they are first used. Otherwise you will have to provide declarations for them before they are used. A compiler must have seen a declaration (or a definition doubling up as a declaration) before it will accept a use of a name.

Look carefully at the output when you use your test program. Unless you have done much better than average, you should notice two things. The first is that the Playpen does not respond to your choices. Two of the choices should make visible changes. We have to call display()for theplaypenobject to make the changes visible.

There are two solutions to this; we can arrange that a function that changes the Playpen always updates the display by callingdisplay()or we can make updating the display a menu option. In the context, I think the former choice is better. Unless you already did this, go back and add app.display()to theclear_playpen()and

plot_pixel()functions.

Now let us look at the second problem. When the request to enter a choice is repeated you are getting a message that says ‘‘is not an option, please try again’’. This is almost certainly baffling you. The problem is that some of the choices require extra information.

After they acquire it they are not clearing out such things as the Return that ended the input.

Thegetline()function is treating this residual data as if it were new input.

There are several ways to fix this but rather than leave you struggling I have provided another function likestd::getline()except that it skips lines that do not contain at least one character that is not whitespace (spaces, tabs and newlines). This function is

fgw::getdata()and is a drop-in replacement forstd::getline()wherever blank lines should be ignored. It is declared infgw text.h. A side effect of usinggetdata()is that it also fixes the problem of assuming the user inputs some data.getdata()will wait for input that is not whitespace.

Please amend the code forget_choice()and test again. You should find that the program now works as expected.

Notice the fallback position that I have provided forchange_plotmode(). This is a common programming idiom called a ‘‘stub function’’. It allows the rest of the code to work

and allows you to take your time with elements that you are not ready to complete. There are various ways thatchange_plotmode()could be written. As we are currently focusing on menus, a menu solution will give you a chance to consolidate your understanding of the menu idiom.

TASK 20

Implement and test thechange_plotmode()function by selecting from a menu of the plot mode options.

Dans le document You Can Do It! (Page 167-172)