The glasses are unmarked; otherwise, it would be easy to pour water from the large, full glass into the medium-sized glass (i.e., the 5-ounce glass) until it contained 4 ounces (which would also leave 4 ounces in the large glass). So, the only way to "measure" the water is to fill the smaller glasses.
Our overall problem is to write a program that simulates a person pouring water from one glass to another while attempting to solve the puzzle. We take the common approach of breaking the overall problem down into a series of smaller steps. The first step is to find a suitable representation for the puzzle problem.
Step 1: Representing The Problem
Pictures help us organize the problem's data and identify the missing information. They also make it easier to see the relations between the different parts of the problem. They needn't be "pretty," accurate, or drawn to scale. Although drawing representations of specific situations occurring within a problem might be beneficial, a picture representing a general situation or condition is frequently more helpful. To that end, we begin by drawing a picture of two arbitrary glasses that do not distinguish between the three different-sized glasses and labeling the various parts of the problem. The following figure generally represents any two glasses joined by the pouring process.
The next step is to solve the problem abstractly and develop an algorithm suitable for programming.
Step 2: Object-Oriented Analysis (OOA)
Previously, we examined three phases that are commonly included in software engineering processes: analysis, design, and implementation. As we analyze the pouring puzzle, we note that the problem centers around three objects: a 3-ounce glass, a 5-ounce glass, and an 8-ounce glass. All three glasses have the same attributes: volume and the amount of water currently in the glass.
All three glasses are similar because they represent the same kind of "thing" and have identical attributes. The volume differentiates one glass object from the others. Therefore, we can abstract or generalize the three glass objects into a single Glass class.
In practice, classes described by a UML class diagram are language-independent. We can translate a UML class to Java, C#, or any object-oriented programming language as easily as we can to C++. It is the task of the programmer to add any needed language-specific detail. But, as we are still learning the subtleties of C++, two slightly different versions of the Glass class are presented. The only difference between the two versions is the pour function argument (i.e., the argument appearing inside the parentheses).
Pass-by-reference
Pass-by-pointer
Glass UML class diagram.
The underlined features belong to the Glassclass as a whole rather than to individual instances of the class (i.e., rather than to individual Glass objects). C++ implements class features by adding the "static" keyword to their definition.
The pour operation changes the amount of water in the destination and source glasses, which it can only do if the program passes the objects by pointer or reference. (Please see Function Argument Passing Summary.) The destination or implicit object is bound to the pour function through the this pointer, which means that it is always passed-by-pointer. But, we can choose how to pass the source or explicit object. The way that we pass the explicit argument to the pour function is the only difference between the two UML diagrams. We generally don't show such implementation details on UML diagrams, which are language-independent. I've included the passing notation in the diagrams to help prepare us for the two solutions presented in the following sections.
Step 3: How Much Water To Pour?
A program is just an expression of an existing solution or algorithm. It's okay to write bits of code to test the various steps in a solution, but writing code without a starting and ending point and some idea of the path between the two is slow, frustrating, and unproductive. We begin developing the problem solution by noting that the relation between the space in the destination class and the amount of water in the source glass determines how much water the pour operation can transfer from one glass into the other. There are two cases to consider:
If the remaining space in the destination glass is less than the amount of water in the source glass, then the space in the destination glass determines the amount of water poured from one glass to the other. We can not add more water to a glass than it can hold.
If the remaining space in the destination glass is greater than the amount of water left in the source glass, then the amount of water in the source glass determines the amount of water poured from one glass to the other. We can not pour more water out of a glass than it contains.
Greeno, Collins, & Resnick (1996) report that "Research comparing excellent adult learners with less capable ones ... [confirms] that the most successful learners elaborate what they read" (p. 19). For computer programming students, one of the elaborating steps is asking, "Is there more than one way to write the code that solves a problem?" The following figure illustrates three ways of writing code to express the solution developed from the two cases above. How we choose to look at a problem affects our solution. The three code fragments represent the same algorithm viewed from slightly different perspectives.
int space = volume - amount;
if (space < source.amount)
{
source.amount -= space;
amount = volume;
}
else
{
amount += source.amount;
source.amount = 0;
}
(a)
int space = volume - amount;
int transfer = (space < source.amount) ? space : source.amount;
amount += transfer;
source.amount -= transfer;
(b)
int space = volume - amount;
int transfer = min(space, source.amount);
amount += transfer;
source.amount -= transfer;
(c)
Calculating how much water to pour.
A direct statement of the case analysis. If the space in the destination glass equals the amount in the source glass, then both branches of the if-statement yield the same results.
The same algorithm is expressed with a conditional operator, which takes the place of the if-statement and calculates the amount of water to transfer or pour. The amount in the destination glass increases by the amount transferred from the destination glass. The amount in the source glass decreases by the same amount.
We can summarize the two cases above by saying that the amount of water transferred or poured from the source glass to the destination glass is the smallest or minimum of the space in the destination and the amount in the source, which we find with the min function. This solution is compact but requires including an additional header file: <algorithm>.
Step 4: How To Manage The Glasses?
We can create a class named Glass that defines a pour function. We can create a pouring-puzzle program using three instances of the Glass class.
source and destination are two instances of the Glass class. The program determines the role an object plays, destination or source, by where it appears in the function call
Any pair of Glass objects can cooperate in the pouring operation. Depending on how the puzzle solver wishes to pour water during that move, either object may be the destination or the source glass. Six combinations of pouring from one glass to another are possible
A more elegant, more compact, and more easily programmed solution is desirable.
Array definition
Passing the source glass
by reference
by pointer
Glass glasses[3]...;
glasses[d].pour(glasses[s]);
glasses[d].pour(&glasses[s]);
Glass* glasses[]...;
glasses[d]->pour(*glasses[s]);
glasses[d]->pour(glasses[s]);
Defining the glasses array and calling the pour function. We can make the array on the stack as an array of objects, or we can make it on the heap as an array of pointers. Regardless of where we make the array, we can choose which passing technique we want by selecting the appropriate operators. To make the summary table easier to display, we allow d and s to represent indexes that select the destination and source glass objects from the array; we also defer the detail represented by the ellipses to the following sections.
Step 5: The Puzzle Logic
Roughly outlining the overall program logic is the final step before programming, which we do with the following logic diagram.
Two similar program versions are developed in the following sections. The first program creates an automatic array of Glass objects on the stack. The second program creates a dynamic array on the heap with the new operator.