4.4 Procedural Abstraction

The cause is hidden, but the result is well known.

OVID, Metamorphoses IV

The Black-Box Analogy

A person who uses a program should not need to know the details of how the program is coded. Imagine how miserable your life would be if you had to know and remember the code for the compiler you use. A program has a job to do, such as compile your program or check the spelling of words in your paper. You need to know what the program’s job is so that you can use the program, but you do not (or at least should not) need to know how the program does its job. A function is like a small program and should be used in a similar way. A programmer who uses a function in a program needs to know what the function does (such as calculate a square root or convert a temperature from degrees Fahrenheit to degrees Celsius) but should not need to know how the function accomplishes its task. This is often referred to as treating the function like a black box.

Calling something a black box is a figure of speech intended to convey the image of a physical device that you know how to use but whose method of operation is a mystery, because it is enclosed in a black box and you cannot see inside the box (and cannot pry it open!). If a function is well designed, the programmer can use the function as if it were a black box. All the programmer needs to know is that if he or she puts appropriate arguments into the black box, then an appropriate returned value will come out of the black box. Designing a function so that it can be used as a black box is sometimes called information hiding to emphasize that the programmer acts as if the body of the function were hidden from view.

Display 4.7 contains the function declaration and two different definitions for a function named newBalance. As the function declaration comment explains, the function newBalance calculates the new balance in a bank account when simple interest is added. For instance, if an account starts with $100, and 4.5 percent interest is posted to the account, then the new balance is $104.50. Hence, the following code will change the value of vacationFund from 100.00 to 104.50:

vacationFund = 100.00;
vacationFund = newBalance(vacationFund, 4.5);

It does not matter which of the implementations of newBalance shown in Display 4.7 that a programmer uses. The two definitions produce functions that return exactly the same values. We may as well place a black box over the body of the function definition so that the programmer does not know which implementation is being used. In order to use the function newbalance, all the programmer needs to read is the function declaration and the accompanying comment.

Display 4.7 Definitions That Are Black-Box Equivalent

Function Declaration

 1   double newBalance(double balancePar, double ratePar);
 2   //Returns the balance in a bank account after
 3   //posting simple interest. The formal parameter balancePar is
 4   //the old balance. The formal parameter ratePar is the interest rate.
 5   //For example, if ratePar is 5.0, then the interest rate is 5 percent
 6   //and so newBalance(100, 5.0) returns 105.00.

Definition 1

double newBalance(double balancePar, double ratePar)
{
    double interestFraction, interest;

    interestFraction = ratePar/100;
    interest = interestFraction * balancePar;
    return (balancePar + interest);
}

Definition 2

double newBalance(double balancePar, double ratePar)
{
    double interestFraction, updatedBalance;

    interestFraction = ratePar/100;
    updatedBalance = balancePar * (1 + interestFraction);
    return updatedBalance;
}

Writing and using functions as if they were black boxes is also called procedural abstraction. When programming in C++ it might make more sense to call it functional abstraction. However, procedure is a more general term than function. Computer scientists use the term procedure for all “function-like” sets of instructions, and so they use the term procedural abstraction. The term abstraction is intended to convey the idea that when you use a function as a black box, you are abstracting away the details of the code contained in the function body. You can call this technique the black-box principle or the principle of procedural abstraction or information hiding. The three terms mean the same thing. Whatever you call this principle, the important point is that you should use it when designing and writing your function definitions.

Programming Tip Choosing Formal Parameter Names

The principle of procedural abstraction says that functions should be self-contained modules that are designed separately from the rest of the program. On large programming projects, a different programmer may be assigned to write each function. The programmer should choose the most meaningful names he or she can find for formal parameters. The arguments that will be substituted for the formal parameters may well be variables in the main part of the program. These variables should also be given meaningful names, often chosen by someone other than the programmer who writes the function definition. This makes it likely that some or all arguments will have the same names as some of the formal parameters. This is perfectly acceptable. No matter what names are chosen for the variables that will be used as arguments, these names will not produce any confusion with the names used for formal parameters. After all, the functions will use only the values of the arguments. When you use a variable as a function argument, the function takes only the value of the variable and disregards the variable name.

Now that you know you have complete freedom in choosing formal parameter names, we will stop placing a "Par" at the end of each formal parameter name. For example, in Display 4.8 we have rewritten the definition for the function totalCost from Display 4.3 so that the formal parameters are named number and price rather than numberPar and pricePar. If you replace the function declaration and definition of the function totalCost that appear in Display 4.3 with the versions in Display 4.8, then the program will perform in exactly the same way, even though there will be formal parameters named number and price and there will be variables in the main part of the program that are also named number and price.

Display 4.8 Simpler Formal Parameter Names

Function Declaration

1     double totalCost(int number, double price);
2     //Computes the total cost, including 5 percent sales tax,
3     //on number items at a cost of price each.

Function Definition

1     double totalCost(int number, double price)
2     {
3        const double TAX_RATE = 0.05; //5 percent sales tax
4        double subtotal;
5        subtotal = price * number;
6        return (subtotal + subtotal * TAX_RATE);
7     }

Programming Tip Nested Loops

When you see nested loops in your code, then you should consider whether or not to apply the principle of procedural abstraction. Consider the explicitly nested loops in Display 3.15 that computed the total number of green-necked vulture eggs counted by all conservationists. We can make this code more readable by moving the loops into procedure calls, as shown in Display 4.9.

Display 4.9 Nicely Nested Loops

 1    //Determines the total number of green-necked vulture eggs
 2    //counted by all conservationists in the conservation district.
 3    #include <iostream>
 4    using namespace std;
 5
 6
 7    int getOneTotal();
 8    //Precondition: User will enter a list of egg counts
 9    //followed by a negative number.
10    //Postcondition: returns a number equal to the sum of all the egg counts.
11
12    int main( )
13    { 
14        cout << "This program tallies conservationist reports\n"
15             << "on the green-necked vulture.\n"
16             << "Each conservationist's report consists of\n"
17             << "a list of numbers. Each number is the count of\n"
18             << "the eggs observed in one"
19             << " green-necked vulture nest.\n"
20             << "This program then tallies"
21             << " the total number of eggs.\n";
22
23        int numberOfReports;
24        cout << "How many conservationist reports are there? ";
25        cin >> numberOfReports;
26
27        int grandTotal = 0, subtotal, count;
28        for (count = 1; count <= numberOfReports; count++)
29        {
30            cout << endl << "Enter the report of "
31                 << "conservationist number " << count << endl;
32            subtotal = getOneTotal();
33            cout << "Total egg count for conservationist "
34                 << " number " << count << " is "
35                 << subtotal << endl;
36            grandTotal = grand_total + subtotal;
37        }
38
39        cout << endl << "Total egg count for all reports = "
40             << grandTotal << endl;
41
42        return 0;
43    }
44
45
46    //Uses iostream:
47    int getOneTotal()
48    {
49        int total;
50        cout << "Enter the number of eggs in each nest.\n"
51             << "Place a negative integer"
52             << " at the end of your list.\n";
53
54        total = 0;
55        int next;
56        cin >> next;
57        while (next >= 0)
58        {
59            total = total + next;
60            cin >> next;
61        }
62        return total;
63    }

Sample Dialogue

This program tallies conservationist reports
on the green-necked vulture.
Each conservationist's report consists of
a list of numbers. Each number is the count of
the eggs observed in one green-necked vulture nest.
This program then tallies the total number of eggs.
How many conservationist reports are there? 3

Enter the report of conservationist number 1
Enter the number of eggs in each nest.
Place a negative integer at the end of your list.
1 0 0 2 −1
Total egg count for conservationist number 1 is 3

Enter the report of conservationist number 2
Enter the number of eggs in each nest.
Place a negative integer at the end of your list.
0 3 1 −1
Total egg count for conservationist number 2 is 4

Enter the report of conservationist number 3
Enter the number of eggs in each nest.

Place a negative integer at the end of your list.
−1
Total egg count for conservationist number 3 is 0

Total egg count for all reports = 7

The two versions of our program for totaling green-necked vulture eggs are equivalent. Both programs produce the same dialogue with the user. However, most people find the version in Display 4.9 easier to understand because the loop body is a function call. When considering the outer loop, you should think of computing the subtotal for one conservationist’s report as a single operation and not think of it as a loop.

Case Study Buying Pizza

The large “economy” size of an item is not always a better buy than the smaller size. This is particularly true when buying pizzas. Pizza sizes are given as the diameter of the pizza in inches. However, the quantity of pizza is determined by the area of the pizza, and the area is not proportional to the diameter. Most people cannot easily estimate the difference in area between a 10-inch pizza and a 12-inch pizza and so cannot easily determine which size is the best buy—that is, which size has the lowest price per square inch. In this case study we will design a program that compares two sizes of pizza to determine which is the better buy.

Problem Definition

The precise specification of the program input and output are as follows:

Input

The input will consist of the diameter in inches and the price for each of two sizes of pizza.

Output

The output will give the cost per square inch for each of the two sizes of pizza and will tell which is the better buy, that is, which has the lowest cost per square inch. (If they are the same cost per square inch, we will consider the smaller one to be the better buy.)

Analysis of the Problem

We will use top-down design to divide the task to be solved by our program into the following subtasks:

  • Subtask 1: Get the input data for both the small and large pizzas.

  • Subtask 2: Compute the price per square inch for the small pizza.

  • Subtask 3: Compute the price per square inch for the large pizza.

  • Subtask 4: Determine which is the better buy.

  • Subtask 5: Output the results.

Notice subtasks 2 and 3. They have two important properties:

  1. They are exactly the same task. The only difference is that they use different data to do the computation. The only things that change between subtask 2 and subtask 3 are the size of the pizza and its price.

  2. The result of subtask 2 and the result of subtask 3 are each a single value: the price per square inch of the pizza.

Whenever a subtask takes some values, such as some numbers, and returns a single value, it is natural to implement the subtask as a function. Whenever two or more such subtasks perform the same computation, they can be implemented as the same function called with different arguments each time it is used. We therefore decide to use a function called unitPrice to compute the price per square inch of a pizza. The function declaration and explanatory comment for this function will be as follows:

double unitPrice(int diameter, double price);
 //Returns the price per square inch of a pizza. The formal
 //parameter named diameter is the diameter of the pizza in
 //inches. The formal parameter named price is the price of
 //the pizza.

Algorithm Design

Subtask 1 is straightforward. The program will simply ask for the input values and store them in four variables, which we will call diameterSmall, diameterLarge, priceSmall, and priceLarge.

Subtask 4 is routine. To determine which pizza is the best buy, we just compare the cost per square inch of the two pizzas using the less-than operator. Subtask 5 is a routine output of the results.

Subtasks 2 and 3 are implemented as calls to the function unitPrice. Next, we design the algorithm for this function. The hard part of the algorithm is determining the area of the pizza. Once we know the area, we can easily determine the price per square inch using division, as follows:

price/area

where area is a variable that holds the area of the pizza. This expression will be the value returned by the function unitPrice. But we still need to formulate a method for computing the area of the pizza.

A pizza is basically a circle (made up of bread, cheese, sauce, and so forth). The area of a circle (and hence of a pizza) is πr2, where r is the radius of the circle and π is the number called “pi,” which is approximately equal to 3.14159. The radius is one half of the diameter.

The algorithm for the function unitPrice can be outlined as follows:

Algorithm Outline for the Function unitPrice

  1. Compute the radius of the pizza.

  2. Compute the area of the pizza using the formula πr2.

  3. Return the value of the expression (price/area).

We will give this outline a bit more detail before translating it into C++ code. We will express this more detailed version of our algorithm in pseudocode. Pseudocode is a mixture of C++ and ordinary English. Pseudocode allows us to make our algorithm precise without worrying about the details of C++ syntax. We can then easily translate our pseudocode into C++ code. In our pseudocode, radius and area will be variables for holding the values indicated by their names.

Pseudocode for the Function unitPrice

radius = one half of diameter;
area = π * radius * radius;
 return (price/area);

That completes our algorithm for unitPrice. We are now ready to convert our solutions to subtasks 1 through 5 into a complete C++ program.

Coding

Coding subtask 1 is routine, so we next consider subtasks 2 and 3. Our program can implement subtasks 2 and 3 by the following two calls to the function unitPrice:

unitPriceSmall = unitPrice(diameterSmall, priceSmall);
unitPriceLarge = unitPrice(diameterLarge, priceLarge);

where unitPriceSmall and unitPriceLarge are two variables of type double. One of the benefits of a function definition is that you can have multiple calls to the function in your program. This saves you the trouble of repeating the same (or almost the same) code. But we still must write the code for the function unitPrice.

When we translate our pseudocode into C++ code, we obtain the following for the body of the function unitPrice:

{//First draft of the function body for unitPrice
     const double PI = 3.14159;
     double radius, area;

    radius = diameter/2;
    area = PI * radius * radius;
     return (price/area);
}

Notice that we made PI a named constant using the modifier const. Also, notice the following line from the code:

radius = diameter/2;

This is just a simple division by 2, and you might think that nothing could be more routine. Yet, as written, this line contains a serious mistake. We want the division to produce the radius of the pizza including any fraction. For example, if we are considering buying the “bad luck special,” which is a 13-inch pizza, then the radius is 6.5 inches. But the variable diameter is of type int. The constant 2 is also of type int. Thus, as we saw in Chapter 2, this line would perform integer division and would compute the radius 13/2 to be 6 instead of the correct value of 6.5, and we would have disregarded a half inch of pizza radius. In all likelihood, this would go unnoticed, but the result could be that millions of subscribers to the Pizza Consumers Union could be wasting their money by buying the wrong size pizza. This is not likely to produce a major worldwide recession, but the program would be failing to accomplish its goal of helping consumers find the best buy. In a more important program, the result of such a simple mistake could be disastrous.

How do we fix this mistake? We want the division by 2 to be regular division that includes any fractional part in the answer. That form of division requires that at least one of the arguments to the division operator / must be of type double. We can use type casting to convert the constant 2 to a value of type double. Recall that static_cast<double>(2), which is called a type casting, converts the int value 2 to a value of type double. Thus, if we replace 2 by static_cast<double>(2), that will change the second argument in the division from type int to type double, and the division will then produce the result we want. The rewritten assignment statement is

radius = diameter/static_cast<double>(2);

The complete corrected code for the function definition of unitPrice, along with the rest of the program, is shown in Display 4.10.

Display 4.10 Buying Pizza

 1     //Determines which of two pizza sizes is the best buy.
 2     #include <iostream>
 3     using namespace std;
 4
 5     double unitPrice (int diameter,double price);
 6     //Returns the price per square inch of a pizza. The formal 
 7     //parameter named diameter is the diameter of the pizza in inches.
 8     //The formal parameter named price is the price of the pizza.
 9
10     int main( )
11     {
12        int unitPriceSmall, unitPriceSmall;
13        double unitPriceSmall, unitPriceSmall,
14            unitPriceLarge, unitPriceLarge;
15
16        cout << "Welcome to the Pizza Consumers Union.\n";
17        cout << "Enter diameter of a small pizza (in inches): ";
18        cin >> diameterSmall;
19        cout << "Enter the price of a small pizza: $";
20        cin >> priceSmall;
21        cout << "Enter diameter of a large pizza (in inches): ";
22        cin >> diameterLarge;
23        cout << "Enter the price of a large pizza: $";
24        cin >> priceLarge;
25
26        unitPriceSmall = unitPrice(diameterSmall, priceSmall);
27        unitPriceLarge = unitPrice(diameterLarge, priceLarge);
28
29        cout.setf(ios::fixed);
30        cout.setf(ios::showpoint);
31        cout.precision(2);
32        cout << "Small pizza:\n"
33             << "Diameter = " << diameterSmall << " inches\n"
34             << "Price = $" << priceSmall
35             << " Per square inch = $" << unitPriceSmall << endl
36             << "Large pizza:\n"
37             << "Diameter = " << diameterLarge << " inches\n"
38             << "Price = $" << priceLarge
39             << " Per square inch = $" << unitPriceLarge << endl;
40        if (unitPriceLarge < unitPriceSmall)
41            cout << "The large one is the better buy.\n";
42        else
43            cout << "The small one is the better buy.\n";
44
45        cout << "Buon Appetito!\n";
46        return 0;
47    }
48
49    double unitPrice(int diameter, double price)
50    {
51        const double PI = 3.14159;
52        double radius, area;
53
54        radius = diameter/static_cast<double>(2);
55        area = PI * radius * radius;
56        return (price/area);
57    }
58

Sample Dialogue

Welcome to the Pizza Consumers Union.
Enter diameter of a small pizza (in inches): 10
Enter the price of a small pizza:  $7.50
Enter diameter of a large pizza (in inches): 13
Enter the price of a large pizza:  $14.75
Small pizza:
Diameter = 10 inches
Price = $7.50 Per square inch = $0.10
Large pizza:
Diameter = 13 inches
Price = $14.75 Per square inch = $0.11
The small one is the better buy.
Buon Appetito!

The type cast static_cast<double>(2) returns the value 2.0, so we could have used the constant 2.0 in place of static_cast<double>(2). Either way, the function unitPrice will return the same value. However, by using static_cast<double>(2), we make it conspicuously obvious that we want to do the version of division that includes the fractional part in its answer. If we instead used 2.0, then when revising or copying the code, we can easily make the mistake of changing 2.0 to 2, and that would produce a subtle problem.

We need to make one more remark about the coding of our program. As you can see in Display 4.10, when we coded tasks 4 and 5, we combined these two tasks into a single section of code consisting of a sequence of cout statements followed by an if-else statement. When two tasks are very simple and are closely related, it sometimes makes sense to combine them into a single task.

Program Testing

Just because a program compiles and produces answers that look right does not mean the program is correct. In order to increase your confidence in your program, you should test it on some input values for which you know the correct answer by some other means, such as working out the answer with paper and pencil or by using a handheld calculator. For example, it does not make sense to buy a 2-inch pizza, but it can still be used as an easy test case for this program. It is an easy test case because it is easy to compute the answer by hand. Let’s calculate the cost per square inch of a 2-inch pizza that sells for $3.14. Since the diameter is 2 inches, the radius is 1 inch. The area of a pizza with radius 1 is 3.14159 * 12, which is 3.14159. If we divide this into the price of $3.14, we find that the price per square inch is 3.14/3.14159, which is approximately $1.00. Of course, this is an absurd size for a pizza and an absurd price for such a small pizza, but it is easy to determine the value that the function unitPrice should return for these arguments.

Having checked your program on this one case, you can have more confidence in it, but you still cannot be certain your program is correct. An incorrect program can sometimes give the correct answer, even though it will give incorrect answers on some other inputs. You may have tested an incorrect program on one of the cases for which the program happens to give the correct output. For example, suppose we had not caught the mistake we discovered when coding the function unitPrice. Suppose we mistakenly used 2 instead of static_cast<double>(2) in the following line:

radius = diameter/static_cast<double>(2);

So that line reads as follows:

radius = diameter/2;

As long as the pizza diameter is an even number, like 2, 8, 10, or 12, the program gives the same answer whether we divide by 2 or by static_cast<double>(2). It is unlikely that it would occur to you to be sure to check both even- and odd-size pizzas. However, if you test your program on several different pizza sizes, then there is a better chance that your test cases will contain samples of the relevant kinds of data.

Programming Tip Use Pseudocode

Algorithms are typically expressed in pseudocode. Pseudocode is a mixture of C++ (or whatever programming language you are using) and ordinary English (or whatever human language you are using). Pseudocode allows you to state your algorithm precisely without having to worrying about all the details of C++ syntax. When the C++ code for a step in your algorithm is obvious, there is little point in stating it in English. When a step is difficult to express in C++, the algorithm will be clearer if the step is expressed in English. You can see an example of pseudocode in the previous case study, where we expressed our algorithm for the function unitPrice in pseudocode.

Self-Test Exercises

  1. What is the purpose of the comment that accompanies a function declaration?

  2. What is the principle of procedural abstraction as applied to function definitions?

  3. What does it mean when we say the programmer who uses a function should be able to treat the function like a black box? (Hint: This question is very closely related to the previous question.)

  4. Carefully describe the process of program testing.

  5. Consider two possible definitions for the function unitPrice. One is the definition given in Display 4.10. The other definition is the same except that the type cast static_cast<double>(2) is replaced with the constant 2.0; in other words, the line

    radius = diameter/static_cast<double>(2);

    is replaced with the line

    radius = diameter/2.0;

    Are these two possible function definitions black-box equivalent?