#6. Ensure entry and exit conditions in loops
There are at least two questions you should always ask yourself while you're writing code for a loop - and ask yourself again after writing it. Here they are:
About the second question, the loop will be exited whenever its condition is evaluated to false. The condition is evaluated once for each iteration. Will it eventually become false? Are you totally sure that this will happen? Can you guarantee it?
If you have other sources of loop exit, such as a break instruction or a return, clearly you are planning different possible exit paths for your loop. Once they happen, will you be able to tell one from another? Is the result of your loop correct for both when the condition becomes false and for the alternative exit paths? Are all the related variables left with the correct values in all cases? Review and test your code to verify that is the case.
There are several sources of infinite loops that you should strive to avoid. Consider this example:
for (unsigned char c = 0 ; c < 256; ++c) {
std::cout << c;
}
The range of the type unsigned char is from 0 to 255. In C++ addition, unsigned integer types wrap around when they exceed their upper numeric limit, so if the value of c is 255, ++c "increments" it to 0. From there it will go up again, if you keep incrementing it, until it reaches 255, from where it will wrap around again to 0, resulting in an infinite loop.
This behaviour of the C++ arithmetic is neither "right" or "wrong" - it's just how the language is specified. You, as a programmer, must be aware of it and write your code so that it does the right things.
Always keep in mind the numeric limits of the types you use. The comparison c < 256 above violates the guideline against implicit type conversions (see Guideline #2), because 256 is obviously not a value of type unsigned char. It will probably cause a compiler warning, such as "Constant out of range in comparison" (and you should fix all compiler warnings), but anyway, you should be aware of the semantics of the types you use in the first place, including their limits. By the way, you should never use magic numbers such as the 256 above.
You will avoid another source of infinite loops if you follow Guideline #5 and avoid playing with the value of the loop counter inside the loop body. Do not modify the loop counter in the loop body, and it will eventually reach the limit value of the loop condition so that it can become false.
Consider now this second example:
int x = 0;
int y = 0;
bool sucess = false;
while (!success) {
success = do(x, y);
++x;
}
Whatever the function do does, and whatever success means, this code doesn't look very safe. You will keep trying forever until something succeeds and do returns true. Are you sure there isn't another terminating condition, such as a maximum time or number of iterations? If you are, go ahead, but it is not a good idea to have a loop in which termination is not evident. Your design should guarantee the termination of the loop, not just suggest it.
If you actually want an infinite loop, because its purpose is to actually run forever or until some external event interrupts it, you should stress it right from the start.
while (true)
{
...
}
Using this style you avoid the confusion between the special loops designed to last forever, and the more common loops which are meant to come to an end.
- Will the loop body be entered at least once?
- Will the loop ever be exited?
About the second question, the loop will be exited whenever its condition is evaluated to false. The condition is evaluated once for each iteration. Will it eventually become false? Are you totally sure that this will happen? Can you guarantee it?
If you have other sources of loop exit, such as a break instruction or a return, clearly you are planning different possible exit paths for your loop. Once they happen, will you be able to tell one from another? Is the result of your loop correct for both when the condition becomes false and for the alternative exit paths? Are all the related variables left with the correct values in all cases? Review and test your code to verify that is the case.
There are several sources of infinite loops that you should strive to avoid. Consider this example:
for (unsigned char c = 0 ; c < 256; ++c) {
std::cout << c;
}
The range of the type unsigned char is from 0 to 255. In C++ addition, unsigned integer types wrap around when they exceed their upper numeric limit, so if the value of c is 255, ++c "increments" it to 0. From there it will go up again, if you keep incrementing it, until it reaches 255, from where it will wrap around again to 0, resulting in an infinite loop.
This behaviour of the C++ arithmetic is neither "right" or "wrong" - it's just how the language is specified. You, as a programmer, must be aware of it and write your code so that it does the right things.
Always keep in mind the numeric limits of the types you use. The comparison c < 256 above violates the guideline against implicit type conversions (see Guideline #2), because 256 is obviously not a value of type unsigned char. It will probably cause a compiler warning, such as "Constant out of range in comparison" (and you should fix all compiler warnings), but anyway, you should be aware of the semantics of the types you use in the first place, including their limits. By the way, you should never use magic numbers such as the 256 above.
You will avoid another source of infinite loops if you follow Guideline #5 and avoid playing with the value of the loop counter inside the loop body. Do not modify the loop counter in the loop body, and it will eventually reach the limit value of the loop condition so that it can become false.
Consider now this second example:
int x = 0;
int y = 0;
bool sucess = false;
while (!success) {
success = do(x, y);
++x;
}
Whatever the function do does, and whatever success means, this code doesn't look very safe. You will keep trying forever until something succeeds and do returns true. Are you sure there isn't another terminating condition, such as a maximum time or number of iterations? If you are, go ahead, but it is not a good idea to have a loop in which termination is not evident. Your design should guarantee the termination of the loop, not just suggest it.
If you actually want an infinite loop, because its purpose is to actually run forever or until some external event interrupts it, you should stress it right from the start.
while (true)
{
...
}
Using this style you avoid the confusion between the special loops designed to last forever, and the more common loops which are meant to come to an end.
Bibliography
[McConnell 2004]
Steve McConnell: Code Complete, 2nd Edition, Microsoft Press, 2004.
This book discusses loops in Chapter 16: "Loops" (pages 367-389).
[Meyer 1997]
Bertrand Meyer: Object-Oriented Software Construction, 2nd Edition, Prentice Hall, 1997.
Meyer introduces the concepts of loop invariant and loop variant in Chapter 11: "Design by Contract: Building reliable software", section 11.12 "Loop invariants and variants". These concepts are thetheoretic background which allows you to actually prove that your loops will come to an end at their due time.
This book discusses loops in Chapter 16: "Loops" (pages 367-389).
[Meyer 1997]
Bertrand Meyer: Object-Oriented Software Construction, 2nd Edition, Prentice Hall, 1997.
Meyer introduces the concepts of loop invariant and loop variant in Chapter 11: "Design by Contract: Building reliable software", section 11.12 "Loop invariants and variants". These concepts are thetheoretic background which allows you to actually prove that your loops will come to an end at their due time.
Comments
Post a Comment