Lab 6: Repetitive Execution


Introduction

We have seen that the C++ if and switch statements use a condition (an expression that evaluates to true or false) to permit a program to execute statements selectively. For example, we could use the code fragment:

   if (op2 != 0)
      return (op1 / op2);
   else
   {
      cout << "\nError: Divide by zero attempted !\n";
      return 0;
   }
and use the condition op2 != 0 to avoid a divide-by-zero error: if op2 is not equal to zero, then the statement before the else is executed; otherwise, the compound statement following the else is executed.

In addition to facilitating selection, most modern programming languages also utilize conditions to permit statements to be executed repeatedly. These repetitive execution statements typically permit a set of statements to be executed over and over, so long as some condition evaluates to true.

As an example use of this capability, suppose that we have a list of lengths that we need to convert from English- to metric-system measurements. Without repetition, converting these lengths requires that we execute the conversion program a separate time for each value on the list. However, by adding a repetitive execution statement to that program, we can convert each value in the list with a single execution of the program.

C++ provides four different loops (well, three, actually) that we will examine in this exercise. These are called the while loop, the do loop, the for loop, and what we call the forever loop.

Getting Started

Today's exercise is to make a calculator program that has more functionality and is more user-friendly than the one we wrote in the last exercise. More precisely, the skeleton program calculate.cpp differs from our previous version by supporting a fifth calculator function: the exponentiation operation. Follow the usual procedure of creating a project for this exercise, save a copy of calculate.cpp there, add calculate.cpp to the project, and then personalize its opening documentation.

You may recall that exponentiation is available in C++ via the function pow() in the cmath library. Just for today (to learn about loops), we will not use pow(), but will instead pretend that we are C++ implementers and write our own exponentiation function, called Power().

Since Power() performs an operation that already exists in a library, we will declare and define it within our source file, rather than in a library.

The Exponentiation Operation

The exponentiation operation should be a familiar one, since the expression

   x^n
performs exponentiation on the base x and the exponent n. We will implement this operation by writing a function Power(), such that a call:
   Power(x, n)
will compute and return x raised to the power n. To simplify our task, we will assume that n is nonnegative.

Function Design

As usual, we begin by using object-centered design to carefully design an exponentiation function. However, before we can describe its behavior, some simple analysis may shed light on what our function must do.

Function Analysis. When faced with a new problem, it is often helpful to solve it "by hand." For example, to calculate Power(2,0), Power(2,1), Power(2,3) and Power(2,5) by hand, we might write:

Power(2,0) Power(2,1) Power(2,2) Power(2,3)
Return 1. 1 * 2 = 2; 1 * 2 = 2; 1 * 2 = 2;
  Return 2; 2 * 2 = 4; 2 * 2 = 4;
    Return 4. 4 * 2 = 8;
      Return 8.

If we examine the pattern in these "by hand" computations, we might construct the following general solution:

   Power(x, n)
   Initialize result to 1.0.
   Set result to result * base. -
   Set result to result * base.  |
   Set result to result * base.  | exponent
   ...                             | times 
   Set result to result * base.  |
   Set result to result * base. -
   Return result.
This provides us with the information needed to describe our function's behavior.

Function Behavior. We can describe what we want to happen as follows:

   Our function should receive a base value and an exponent value
   from the caller of the function.  It should initialize result
   to one, and then repeatedly multiple result by the base value,
   with the number of repetitions being the exponent value. 
   Our function should then return result.

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

Description Type Kind Movement Name
The base value double varying received base
The exponent value int varying received exponent
The result value double varying received result

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

   Receive: base, a double;
            exponent, an int.
   Return: result, a double = base^exponent.

Using this specification, go to calculate.cpp and replace the line

   // ... replace this line with the prototype of Power() ...
with a prototype of Power(), and replace the line
   // ... replace this line with the definition of Power() ...
with a function stub for Power(). Then uncomment the call to Power() within Apply() and use the compiler to test the syntax of what you have written.

Note that the call to Power() uses int() to convert the double argument op2 to an integer. Without this conversion, our attempt to pass a double argument into an int parameter would result in a compilation error.

Function Operations. From our behavioral description, we have the following operations:

Description Defined? Name Library?
1 Receive base and exponent yes function call
mechanism
built-in
2 Initialize result to 1.0 yes declaration built-in
3 Set result to result*base yes *= built-in
4 Repeat (3), counting from 1 to exponent yes for built-in
5 Return result yes return built-in

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

   0. Receive base and exponent from caller.
   1. Initialize result to 1.0;
   2. For each count from 1 to exponent:
       result *= base.
   3. Return result.

Coding

As we have seen before, the C++ for loop is designed to facilitate the counting behavior required by step 2. Recall that its simplified pattern is:

   for (Type Var = Start; Var <= Stop; Var++)
      Statement
where Type is a C++ numeric type, Var is the loop control variable used to do the counting, Start is the first value, and Stop is the last value. Using this information, complete the stub of function Power(). Then compile and test calculate.cpp, checking that the exponentiation works for a variety of values.

Characterizing Loops

The pattern for a C++ for loop is actually more general than we have indicated:

   for (InitializationExpr; Condition; StepExpr)
      Statement
where InitializationExpr is any initialization expression, Condition is any boolean expression, and StepExpr is an arbitrary expression. For example, if for some reason we wanted to count downwards and output the multiples of 12 from 100 to 1, then we could write:
   for (int i = 100; i >= 1; i--)
      cout << 12 * i << endl;
Such a loop will continue to execute so long as the condition i >= 1 evaluates to true.

C++ for loops are thus controlled by conditions, just as if statements are controlled by conditions. As we shall see, each of the other C++ loop statements are also controlled by conditions.

A loop may be categorized by when it evaluates its condition, with respect to the statements it repeats:

  1. Pretest loops evaluate their condition before the loop's statements.
  2. Posttest loops evaluate their condition after the loop's statements.
  3. Unrestricted loops evaluate their condition before, after, or within the loop's statements.
The for loop is a pretest loop, because it evaluates its condition before the loop's statement is executed (don't take my word for it -- check back and see!) However, the for loop is designed primarily for problems that involve counting through ranges of numbers, or problems in which the number of repetitions can be determined in advance. As we shall see, there are many problems that do not fit this pattern.

The other three C++ loops differ from the for loop in that they are general-purpose loops, designed for problems where the number of repetitions is not known in advance. Why are there three of them? Because

  1. The C++ while loop provides a general pretest loop;
  2. The C++ do loop provides a general posttest loop; and
  3. The C++ forever loop provides a general unrestricted loop.
To see why this is significant, let's examine the problem of getting a menu-choice and ensuring that it is valid.

Getting a Valid Menu Choice

The program in calculate.cpp uses function GetMenuChoice() to get the user's menu choice:

   char GetMenuChoice(const string MENU)
   {
     cout << MENU;
     char choice;
     cin >> choice;
     return choice;
   }
Getting a menu choice is a potential source of user error, because the user could enter an invalid choice.

One way to handle such errors is to repeatedly display the menu and input the user's choice, so long as they continue to enter invalid menu choices. Since there is no way to predict in advance how many erroneous choices the user may enter, the for loop is not an appropriate statement to use in this situation.

The general-purpose loops give us a way to handle user errors, but we must decide which one to use. To do so, we can begin by writing a partial algorithm for this problem using a generic Loop statement, in which we don't worry (for the moment) how control will leave the loop:

   Loop:
      a. Display MENU.
      b. Read choice.
   End loop.
   Return choice.
Every loop has a continuation condition, that defines the circumstances under which we want repetition to continue; and a termination condition, that defines the circumstances under which repetition should terminate. (A continuation condition is usually the negation of the termination condition.) To determine how control should leave the loop, we must identify these conditions. For this particular problem, we want repetition to continue so long as
   choice is an invalid menu choice
and terminate when
   choice is a valid menu choice
so these are our continuation and termination conditions, respectively.

Since choice is not known until after Step (b), following (b) is the logical place to exit the loop, which we can describe using our termination condition as follows:

   Loop:
      a. Display MENU.
      b. Read choice.
      c. If choice is a valid menu choice, exit the loop.
   End loop.
   Return choice.
In this algorithm, it is apparent that the controlling condition is evaluated at the bottom of the loop, which implies that a post-test loop is the appropriate loop to choose.

In C++, the post-test loop is called the do loop, and its pattern is as follows:

   do
   {
      Statements
   }
   while ( Condition );
When execution reaches such a loop, the following actions occur:
  1. Statements execute.
  2. Condition is evaluated.
  3. If Condition is true: control returns to step 1;
    Otherwise, control proceeds to the next statement after the loop.
Note that since repetition continues so long as the condition is true, a do loop uses a continuation condition.

Since its Condition is not evaluated until after the first execution of Statements, the do loop guarantees that Statements will be executed one or more times. For this reason, the do loop is said to exhibit one-trip behavior. We will therefore implement the preceding algorithm using a do loop.

The notion of a "valid" menu choice is a bit tricky. One way to handle it is to require that the valid menu choices be consecutive letters of the alphabet (e.g., 'a' through 'e'). If we then pass the first and last valid choices to GetMenuChoice() as arguments:

   char operator = GetMenuChoice(MENU, 'a', 'e');
and add parameters to the function prototype and definition to store these arguments:
   char GetMenuChoice(const string MENU, char firstChoice, char lastChoice);
Since firstChoice and lastChoice will define the range of valid choices, we can express our loop's continuation condition in terms of those values: choice is invalid if and only if
   choice < firstChoice || choice > lastChoice
We are then ready to add a do loop to the function to implement our algorithm:
   char GetMenuChoice(const string MENU, char firstChoice, char lastChoice)
   {
     char choice;
     do
     {
       cout << MENU;
       cin >> choice;
     }
     while (choice < firstChoice || choice > lastChoice);
     
     return choice;
   }
Note the necessity of not declaring choice within the loop, since doing so would make it local to the loop, and inaccessible by the return statement.

Make the necessary modifications to calculate.cpp so that GetMenuChoice() will only return a valid menu choice. Then thoroughly test your code with invalid and valid values. Continue when it works correctly.

Getting Valid Numeric Input

Another potential source of error occurs when our program reads values for op1 and op2 from the keyboard -- the user might enter some non-numeric value. Our current version checks for this using the assert() mechanism and the good() function member of cin:

   cout << "\nNow enter your operands: ";
   double op1, op2;
   cin >> op1 >> op2;
   assert(cin.good());
But suppose we wished to give the user another chance to enter valid values, instead of just terminating the program? At first glance, it looks like we could use the same approach as before, by replacing the assert() with a posttest loop that repeats the steps so long as good() returns false:
   do
   {
	// prompt for Op1 and Op2
	// input Op1 and Op2
   }
   while ( !cin.good() );
However, this approach is inappropriate because of two subtleties about numeric I/O. When the >> operator is expecting a real value, but gets a non-real value, two things are true, each of which causes a difficulty:
  1. The status of cin is set so that its member function good() will return false (0). This causes a two-fold difficulty:
    1. No input operations can be performed with cin so long as cin.good() returns false.
    2. In order to stop cin.good() from returning false, cin must be explicitly cleared using its clear() member function.
    Together, these imply that we must call cin.clear() before attempting another input operation, or else an infinite loop will result.
  2. The non-real input value is left (unread) in the input stream. This means that even after we have cleared the status of cin, the unread input is still sitting in the stream waiting to be read. That is, when the input operation is performed the next repetition of our loop, it will re-read the same non-real value that is still sitting in cin. What we need is a way to somehow "skip over" that value. This can be accomplished using a different function member of cin named ignore(), whose pattern is
          cin.ignore( NumChars, UntilChar );
    When execution reaches this statement, characters in cin will be skipped until NumChars have been skipped, or the character UntilChar is encountered, whichever comes first. Since lines are usually not more than 120 characters in length, and the end of a line of input is marked by the newline character ('\n'), we can use the following call to solve this difficulty:
          cin.ignore(120, '\n');
    Such a call will skip 120 characters in cin, or until a newline character is encountered, whichever comes first.
All of this complicates our choice of which loop to use, because we now have four steps to perform:
   a. Display a prompt for two real values; // one or more times
   b. Input op1 and op2;                   // one or more times
   c. cin.clear();                          // only if necessary (i.e., zero or more times)
   d. cin,ignore(120, '\n');                // only if necessary (i.e., zero or more times)
This poses a dilemma. Some of the steps should be executed at least once, and others zero or more times. Which loop do we choose?

One traditional solution is modify our algorithm to use a pre-test loop in the following way:

   a. Display a prompt for two real values;
   b. Input op1 and op2;
   c. Loop (pretest), so long as cin.good() is false:
      1) cin.clear()
      2) cin.ignore(120, '\n');
      3) Display a prompt for two real values;
      4) Input op1 and op2;
      End loop.
By performing steps a and b outside of the loop, we ensure they are executed at least once. By performing steps 1), 2), 3) and 4) inside the pretest loop, they will be executed zero times, if the user enters a valid real value in step b.

The drawback to this approach is its redundance: steps 3) and 4) are exactly the same as steps a and b. This is not too much of an inefficiency, so long as one does not mind the extra typing. In the final part of this exercise, we will see a way to avoid this redundancy.

Like many other languages the C++ pretest loop is called the while loop, and its pattern is as follows:

   while ( Condition )
      Statement
As usual, Condition is any C++ expression that evaluates to true or false, and Statement can be either a single or compound C++ statement. Statement is often referred to as the body of the loop.

When execution reaches this statement, the following actions occur:

   I. Condition is evaluated (before Statement is executed);
   II. If Condition evaluates to false, then
         Statement is bypassed, and control proceeds to the statement after the loop;
      Otherwise
         A. Statement is executed; and
         B. execution then returns to step I;
      End if.
Since Statement may be executed zero or more times, the while loop is said to exhibit zero-trip behavior (i.e., the Statement controlled by a while loop may go unexecuted, if Condition is initially false).

In calculate.cpp, add a while loop to encode step c of our modified algorithm, as well as the statements to encode steps 1), 2), 3) and 4). Note that the condition

   !cin.good()
can be used to control the while loop. Note also that since the while loop must repeat multiple statements, its Statement must be a compound statement or else an infinite loop may result.

Then translate and thoroughly test what you have written. If you should happen to generate an infinite loop, it can usually be terminated by typing Ctrl-C (pressing the Ctrl and C keys simultaneously). On some systems, it may be necessary to type Ctrl-C several times, or to press Enter and then Ctrl-C. Continue when what you have written makes the entry of numeric values "fool-proof."

One Execution, Multiple Calculations

The algorithm that our program is using is basically as follows:

   0. Display a greeting.
   1. Display a menu of operations and read operation,
       guaranteed to be a valid menu operation.
   2. Display a prompt for two real values;
   3. Read op1 and op2.
   4. Loop (pretest), so long as a real value was not entered:
      a. Clear the status of cin;
      b. Skip the invalid input;
      c. Display a prompt for two real values;
      d. Input Op1 and Op2;
      End loop.
   5. Compute result by applying operation to op1 and op2.
   6. Display result.
Using this algorithm, we have to re-run our program for every calculation, which is inconvenient for the user. A more convenient calculator would "wrap" some of the statements in a loop, so that the user could perform multiple calculations without having to re-run the program. To do so, we might modify our algorithm:
   0. Display a greeting.
   1. Loop:
      a. Display a menu of operations and read operation,
          guaranteed to be a valid menu operation.
      b. Display a prompt for two real values;
      c. Read op1 and op2.
      d. Loop (pretest), so long as a real value was not entered:
         1) Clear the status of cin;
         2) Skip the invalid input;
         3) Display a prompt for two real values;
         4) Input Op1 and Op2;
         End loop.
      e. Compute result by applying operation to op1 and op2.
      f. Display result.
      End loop.
In order to determine which kind of loop to use, we must determine where to evaluate the loop's termination condition, which we might express as:
   the user does not want to quit
One way to have the user indicate that they want to quit is to view quitting as an operation, and provide an additional menu choice (i.e., 'f') by which the user can indicate that they want to quit. The condition
   operation == 'f'
thus evaluates to true if the user wishes to quit.

So when do we check this condition? The ideal place is to evaluate it as soon as it can be known, namely immediately following the input of operation:

Put differently, if the user wants to quit, steps b through f should not be executed -- execution should immediately leave the loop. We thus have a situation where the loop's condition should not be evaluated at the loop's beginning (eliminating the pretest loop), nor at its end (eliminating the posttest loop), but in its middle (suggesting the unrestricted loop):

   0. Display a greeting.
   1. Loop:
      a. Display a menu of operations and read operation,
          guaranteed to be a valid menu operation.
      b. If operation is quit, exit the loop.
      c. Display a prompt for two real values;
      d. Read op1 and op2.
      e. Loop (pretest), so long as a real value was not entered:
         1) Clear the status of cin;
         2) Skip the invalid input;
         3) Display a prompt for two real values;
         4) Input Op1 and Op2;
         End loop.
      f. Compute result by applying operation to op1 and op2.
      g. Display result.
      End loop.

In C++, the unrestricted loop is a simplification of the for loop that we call the forever loop, that has the following pattern:
   for (;;)
   {
      StatementList1

      if ( Condition ) break;

      StatementList2
   }
That is, by leaving out the three expressions that normally control a for loop, it becomes an infinite loop. Of course, we do not want an infinite number of repetitions, but instead want execution to leave the loop when a termination condition becomes true. As shown in the pattern above, this can be accomplished by placing an if statement that uses a termination condition to control a break statement (called an if-break combination) within the body of the loop.

When a break statement is executed, execution is immediately transferred out of the forever loop to the first statement following the loop. By only selecting the break statement when Condition is true, a forever loop's repetition can be controlled in a manner similar to the other general-purpose loops.

As a result, this loop can be used to solve our problem, by making StatementList1 step a of our algorithm, and making StatementList2 steps b through f. Modify your source program to incorporate this approach, and then translate and test the newly added statements.

Note that this loop can be used to "Get Valid Numeric Input" without redundant statements:

   for (;;)                              // LOOP:
   {
      cout << "\nEnter two operands: ";  //  executed one or more times
      cin >> op1 >> op2;                 //  executed one or more times

      if ( cin.good() ) break;           //  if numbers entered, exit the loop

      cin.clear();                       //  executed zero or more times
      cin.ignore(120, '\n');             //  executed zero or more times
   }                                     // End Loop.
Because it eliminates redundant coding, some programmers prefer the forever loop in situations where some statements must be executed zero or more times and other statements must be executed one or more times.

Phrases you should now understand:

Condition, Repetitive Execution, For Loop, While Loop, Do Loop, Forever Loop, General-Purpose Loop, Counting Loop, Zero-Trip Behavior, One-Trip Behavior, Pretest Loop, Posttest Loop, Unrestricted Loop, Break Statement, If-Break Combination.


Submit:

A hard copy of your final version of calculate.cpp and an execution record showing its execution.


Back to This Lab's Home Page

Back to the Prelab Questions

Forward to the Homework Projects


Copyright 1998 by Joel C. Adams. All rights reserved.