Lab 6: Selection


Introduction

One of the easiest ways to make a program user-friendly is to make it menu driven. That is, rather than prompting the user in some vague sort of way, we present them with a menu of the choices available to them. Then all the user has to do is look at the menu and choose one of the choices. Since the menu always tells the user what their options are, the user needs no special knowledge, making such a program easy to use.

For example, a simple 4-function calculator program might prompt the user by providing the following menu:

Please enter:
  + to add two numbers.
  - to subtract two numbers.
  * to multiple two numbers.
  / to divide two numbers.
Thanks to the menu, a user knows exactly what to enter.

This lab's exercise is to complete such a program, and at the same time learn about some of the C++ control structures for selection.

Old Code

Let's first take a look at some old code. In the previous lab, we looked at this function:

string pluralize (string singularNoun)
{
  int lastCharIndex = singularNoun.size() - 1;
  char lastChar = singularNoun[lastCharIndex];
  if ((lastChar == 's') || (lastChar == 'x'))
    return singularNoun + "es";
  else if (lastChar == 'y')
    {
      string base = singularNoun.substr (0, lastCharIndex);
      return base + "ies";
    }
  else
    return singularNoun + "s";
}

This function used a multi-branch if to decide which rule to apply in making the noun plural.

General if Statement

The general pattern for an if statement looks like this:

if ( Condition )
  Statement1
[else
  Statement2 ]

Either Statement1 or Statement2, but not both, are executed based on the value of Condition. If Condition is true, then Statement1 is executed; otherwise (i.e., Condition is false), Statement2 is executed.

The square brackets [...] in the pattern indicates that the else clause is optional. Sometimes a simple if, without an else, is all we need.

Multi-branch if Statement

We can also chain several ifs together into one statement. Now generally it's not advisable to put an if statement in as Statement1 of another if. These can get confusing. However, using an if statement for Statement2 is perfectly acceptable---encouraged, even. In general, a multi-branch if looks like this:

if (Condition1)
  Statement1
else if (Condition2)
  Statement2
...
else if (ConditionN)
  StatementN
else
  StatementN+1

Consider the pluralizer above. There are four possibilities for the singular noun: it ends in an 's', it ends in an 'x', it ends in a 'y', or it ends in something else. This chain of rules/conditions leads to a chain of ifs.

Exactly one of the StatementI will be executed. It will be the first StatementI where ConditionI is true. All of the previous conditions must be false. If all conditions fail, then the fail-safe StatementN+1 is executed. This is exactly what we want and need for the pluralizer; you'll also need it for the code in this lab.

Helpful hint: Include a fail-safe else in a multibranch if, even if it's just a debugging statement.

The debugging statement can be as simple as a statement that prints an error message like "This statement should not print". This will make things much easier for you to debug your program when (not if) something goes wrong.

Also, there is no condition after the last else.

Helpful hint: The last else in any if statement should not have a condition test.

When any else clause is triggered, we know that the previous test failed, so there's no need to make a further test. For example, we could write:

if ((lastChar == 's') || (lastChar == 'x'))
  return singularNoun + "es";
else if ((lastChar != 's') && (lastChar != 'x') && (lastChar == 'y'))
{
  string base = singularNoun.substr (0, lastCharIndex);
  return base + "ies";
}
else if ((lastChar != 's') && (lastChar != 'x') && (lastChar != 'y'))
  return singularNoun + "s";

But the extra tests in this version are completely unnecessary. When any of the != tests are evaluated in this new multi-branch if, they will always be true because they only made after the corresponding == tests were discovered false. This is why the original version of pluralize() does not have these unnecessary != tests.

Compound Statements

The patterns above for the if and multi-branch if statements were carefully crafted. In particular, we used the singular "Statement", not "Statements". We can put only one statement in those places in the patterns.

What if we need more than one statement? Well, that's exactly what we needed with the second rule for pluralizing a word which involves two statements. C++ allows us to wrap several statements in curly braces and treat them as one:

{
  Statement1
  Statement2
  ...
  StatementN
}

So that's why there are those curly braces around the two statements for the second rule. If we dropped them, then the compiler would get confused over the last else since there isn't a corresponding if close enough for it.

Files

Directory: lab6

Create the specified directory, and copy the files above into the new directory. Only 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.

Looking at the Code

Take a few moments to study calculate.cpp, particularly the code in the main() routine. Make sure you understand the purpose of each statement in calculate.cpp before going any further in this lab.

Ignoring its error-checking code, main() should behave like so:

Our program should display on the screen a greeting, followed by a menu of permissible operations. It should then read a user-specified operation from the keyboard. It should then prompt the user for the two operands for that operation, and then read those operands from the keyboard. It should then compute the result of applying the user-specified operation to the two operands. It should conclude by displaying that result on the screen, with appropriate labeling.

All of this behavior is coded up in main() expect for the sentence written in boldface. Since there is no predefined C++ capability to directly perform that operation, we will write a function apply() to do that action. This function is (arguably) useful beyond just this program, so we'll put it off in a library.

Function Design

As usual, we use object-centered design to develop this function.

Behavior. Our function must apply operation to op1 and op2 and return the result. We can describe the needed behavior as follows:

Our function should receive from its caller an operation, and two operands. If the operation is '+', our function should return the sum of the two operands. If the operation is '-', our function should return their difference. If the operation is '*', our function should return their product of the two operands. If the operation is '/', our function should return their quotient.

Objects. From this behavioral description, we can identify the following objects:

Description Type Kind Movement Name
The operation char varying received operation
One of the operands double varying received op1
The other operand double varying received op2
The sum of the operands double varying out op1 + op2
The difference of the operands double varying out op1 - op2
The product of the operands double varying out op1 * op2
The quotient of the operands double varying out op1 / op2

From this list, we can specify the behavior of our function as follows:

Specification:
receive: operation, a char; op1 and op2, two double values.
return: the double result of applying operation to op1 and op2.

Please, please, please save yourself much pain and watch the name of the first parameter:

Helpful hint: Do not name the first parameter operator. It's a keyword, and the compiler may not give you the best error messages if you do use it as a variable.

Using the specification (and heeding the hint), add a prototype for a function named apply() in mathops.h and a stub in mathops.cpp.

Function Operations From our behavioral description, we have these operations:

Description Predefined? Name Library
Receive operation (a char) yes function call
mechanism
built-in
Receive op1 (a double) yes function call
mechanism
built-in
Receive op2 (a double) yes function call
mechanism
built-in
Return the... yes return built-in
...sum of op1 and op2 yes + built-in
...difference of op1 and op2 yes - built-in
...product of op1 and op2 yes * built-in
...quotient of op1 and op2 yes / built-in
Do exactly one of math operations yes if statement built-in

C++ provides facilities for performing each of these operations as noted in this chart. The last operation is different from the others, in that it requires selective behavior. We can use the if statement.

Function Algorithm. We can organize these operations into the following algorithm:

  1. Receive operation, op1 and op2.
  2. If operation is '+':
        Return op1 + op2.
    Otherwise, if operation is '-':
        Return op1 - op2.
    Otherwise, if operation is '*':
        Return op1 * op2.
    Otherwise, if operation is '/':
        Return op1 / op2.
    Otherwise,
        Print an error message and return 0.

From this algorithm it is clear that a multi-branch if will do the trick. The last case is technically unnecessary since our program checks the operation, but as noted above you shouldn't rely on that. Print out an error message here so that it's very clear that something went wrong. If you want, you can even quit the program (with exit(1);) instead of returning 0, which is merely an arbitrary value in this case.

Add a multi-branch if to your apply() stub to encode this step.

Testing and Debugging.

When you are done, compile everything and test your program. Fix it up until it works correctly. Be sure to test each operator at least twice.

Coding 2: The switch Statement

The multi-branch if suffers from one drawback:

In general, selecting Statementi using a multi-branch if statement requires the evaluation of i conditions. This can be good because we can assume that the failed test did, in fact, fail. When testing for ranges of values, this is incredibly useful (and even necessary).

On the other hand, the evaluation of each condition consumes time, statements that occur later in the multi-branch if statement take longer to execute than do statements that occur earlier. It would be great if we could avoid this if at all possible. One possibility is using a switch statement.

A switch statement looks like this:

switch ( ConstantExpression )
{
CaseList1
  StatementList1
CaseList2
  StatementList2
...
CaseListN
  StatementListN
default:
  StatementListN+1
}

ConstantExpression is any C++ expression that evaluates to a integer-compatible constant. Each StatementListi is a sequence of valid C++ statements. Each CaseListi is one or more cases of the form:

case Constant :
Constant is an integer-compatible constant.

That's a fair bit of code. Watch the careful use of terminology here, particularly "integer-compatible constant". Let's first figure out what a switch statement does:

  1. The ConstantExpression is evaluated.
  2. If the value of the expression is present in CaseListI, then execution begins in StatementListI and proceeds, until a break statement, a return statement, or the end of the switch statement is encountered.
  3. If the value of ConstantExpression is not present in any CaseListI, then the (optional) default StatementListN+1 is executed.

A given Constant can appear in only one CaseListi.

Helpful hint: Just as a multi-branch if should always have a fail-safe else clause, you should always have a default case in a switch statement, even if it just prints an error message.

Perhaps the trickiest thing about a switch statement is the purpose of the cases. The cases of the switch statement are merely labels for the compiler to tell it where to start executing code. Consider this code:

switch (i)
{
case 0:
  cout << "Hi ";
case 1:
  cout << "there, ";
default:
  cout << "people!";
}

Then, if i is 0, this prints

Hi there, people!

The cases tell the compiler where to start executing the code, not when to stop. While this is useful in some situations, it won't be for us. When I make a noun plural, I want to apply only one of the rules. When I add two numbers together, I don't also want to multiply them. To stop executing the code at the end of a statement list, we can the statement list with a break statement, a return statement, or a call to exit().

We've used returns for returning values from a function. We've also use the exit() function for stopping the program in case of an error. The break statement is even simpler:

break;

Helpful hint: The statement list of every case list in a switch statement should end with a break, a return, or an exit().

So when would we use a switch statement? Whenever we can. Unfortunately, there are a few restrictions:

The integer-compatible data types in C++ include int (of course), char, and bool. It does not include double or string. That's very important, so maybe you should read it again, this time out loud: "integer-compatible" does not include double or string.

Consider the chart below. If we write the algorithm on the left, we can use the switch in the middle which is equivalent to the multi-branch if on the right:

Set Variable to ConstantExpression.
If (Variable  is equal to Value1) then
  StatementList1
Else if (Variable is equal to  Value2) then
  StatementList2
...
Else if (Variable is equal to ValueN) then
  StatementListN
Else
  StatementListN+1
End if.
switch (ConstantExpression)
{
case Value1:
  StatementList1
  break;
case Value2:
  StatementList2
  break;
...
case ValueN:
  StatementListN
  break;
default:
  StatementListN+1
}
Variable = ConstantExpression;
if (Variable == Value1)
{
  StatementList1
}
else if (Variable == Value2)
{
  StatementList2
}
...
else if (Variable == ValueN)
{
  StatementListN
}
else
{
  StatementListN+1
}

A switch statement is more efficient than the multi-branch if statement because a switch statement can select StatementListI in the time it takes to evaluate one condition. The switch statement thus eliminates the non-uniform execution time of statements controlled by a multi-branch if statement. But since the switch statement has so many restrictions on it, we use it only occasionally.

We have a slight wrinkle with pluralize() because it's first test actually tests two things. However, both tests are equality tests with constants and one variable. We can use two cases for one statement list in a switch statement.

So we could rewrite pluralize() like so:

string pluralize (string singularNoun)
{
  int lastCharIndex = singularNoun.size() - 1;
  char lastChar = singularNoun[lastCharIndex];
  switch (lastChar)
  {
  case 's':
  case 'x':
    return singularNoun + "es";
  case 'y':
    string base = singularNoun.substr (0, lastCharIndex);
    return base + "ies";
  default:
    return singularNoun + "s";
  }
}

We're careful in this code to end each statement list with a return. Each case has an integer-compatible (i.e., char) constant.

Try it out for yourself,

  1. Copy the multi-branch if version of apply() so that you have two copies of it in the same file.
  2. Rename one of them to be apply2().
  3. Now replace the multi-branch if in apply() with an equivalent switch statement. (Leave apply2() alone.)
Translate and test what you have written.

When everything is working right, save your work and print a hard copy of your code.

Submit

Turn in your code and sample runs that proves your calculator works for all operations.

Terminology

case, control structure, integer-compatible constant, menu, menu driven, selection, selective behavior
Lab Home Page | Prelab Questions | Homework Projects
© 2003 by Prentice Hall. All rights reserved.
Report all errors to Jeremy D. Frens.