Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
Writing perfect software may be an elusive goal for
developers, but a few defensive techniques, routinely applied, can go a long
way toward improving the quality of your code.
Although the complexity of typical production software
guarantees that testers will always have a job, we hope you still yearn to
produce defect-free software. Object-oriented design techniques do much to
corral the difficulty of large projects, but eventually you must write loops
and functions. These details of programming in the small become the building
blocks of the larger components needed for your designs. If your loops are off
by one or your functions calculate the correct values only most of the time,
you re in trouble no matter how fancy your overall methodology. In this chapter, you ll see practices that help create robust code regardless of the size of your
project.
Your code is, among other things, an expression of your attempt
to solve a problem. It should be clear to the reader (including yourself)
exactly what you were thinking when you designed that loop. At certain points
in your program, you should be able to make bold statements that some condition
or other holds. (If you can t, you really haven t yet solved the problem.) Such
statements are called invariants, since they should invariably be true
at the point where they appear in the code; if not, either your design is faulty,
or your code does not accurately reflect your design.
Consider a program that plays the guessing game of Hi-Lo. One
person thinks of a number between 1 and 100, and the other person guesses the
number. (We ll let the computer do the guessing.) The person who holds the
number tells the guesser whether their guess is high, low or correct. The best
strategy for the guesser is a binary search, which chooses the midpoint
of the range of numbers where the sought-after number resides. The high-low
response tells the guesser which half of the list holds the number, and the
process repeats, halving the size of the active search range on each iteration.
So how do you write a loop to drive the repetition properly? It s not
sufficient to just say
bool guessed = false;
while(!guessed) {
...
}
because a malicious user might respond deceitfully, and you
could spend all day guessing. What assumption, however simple, are you making
each time you guess? In other words, what condition should hold by design
on each loop iteration?
The simple assumption is that the secret number is within
the current active range of unguessed numbers: [1, 100]. Suppose we label the
endpoints of the range with the variables low and high.
Each time you pass through the loop you need to make sure that if the number
was in the range [low, high] at the beginning of the loop, you
calculate the new range so that it still contains the number at the end of the
current loop iteration.
The goal is to express the loop invariant in code so that a
violation can be detected at runtime. Unfortunately, since the computer doesn t
know the secret number, you can t express this condition directly in code, but
you can at least make a comment to that effect:
while(!guessed) {
// INVARIANT: the number is in the range [low, high]
...
}
What happens when the user says that a guess is too high or
too low when it isn t? The deception will exclude the secret number from the
new subrange. Because one lie always leads to another, eventually your range
will diminish to nothing (since you shrink it by half each time and the secret
number isn t in there). We can express this condition in the following program:
//: C02:HiLo.cpp {RunByHand}
// Plays the game of Hi-Lo to illustrate a loop
invariant.
#include <cstdlib>
#include <iostream>
#include <string>
using namespace std;
int main() {
cout << "Think of a number between 1 and
100" << endl
<< "I will make a
guess; "
<< "tell me if I'm
(H)igh or (L)ow" << endl;
int low = 1, high = 100;
bool guessed = false;
while(!guessed) {
// Invariant: the number is in the range [low,
high]
if(low > high) { // Invariant violation
cout << "You cheated! I quit"
<< endl;
return EXIT_FAILURE;
}
int guess = (low + high) / 2;
cout << "My guess is " <<
guess << ". ";
cout << "(H)igh, (L)ow, or (E)qual?
";
string response;
cin >> response;
switch(toupper(response[0])) {
case 'H':
high = guess - 1;
break;
case 'L':
low = guess + 1;
break;
case 'E':
guessed = true;
break;
default:
cout << "Invalid response"
<< endl;
continue;
}
}
cout << "I got it!" << endl;
return EXIT_SUCCESS;
} ///:~
The violation of the invariant is detected with the
condition if(low > high), because if the user always tells the truth,
we will always find the secret number before we run out of guesses.
We also use a standard C technique for reporting program
status to the calling context by returning different values from main( ).
It is portable to use the statement return 0; to indicate success, but
there is no portable value to indicate failure. For this reason we use the
macro declared for this purpose in <cstdlib>: EXIT_FAILURE.
For consistency, whenever we use EXIT_FAILURE we also use EXIT_SUCCESS,
even though the latter is always defined as zero.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |