Object-Oriented C++ from scratch: Defensive Programming
- By Jesse Liberty
- February 29, 2000
WHEN ROBERT MARTIN first talked with me about this column, it was clear that the idea was to provide a safe harbor within C++ Report
for the novice C++ programmer. After all, most of the articles are quite advanced and the neophyte could easily be intimidated by some of the more esoteric topics covered in a typical issue.
As I wrote my columns, however, I moved inexorably, almost inadvertently, from more introductory material to advanced topics. It is time to get back to basics.
This month's column is on defensive programming: writing your programs to troubleshoot problems and bugs early; before they ship to your customer.
Asserting What You Believe It is not uncommon for bugs to linger because a section of code is not behaving as you think it is. It can be difficult at times to get a handle on what is going wrong, because we tend to see what we expect to see. Look at Figure 1, and read it to yourself. Now read it aloud. Do you see what is wrong? It sometimes takes a few readings. If you can't see the problem, put your finger on each word as you read it.
Many people need to read this three or four times before they see the problem. Similarly, I've stared at a line of code for minutes on end before I've been able to find a simple typo. More complicated bugs can be murder to find because, it often turns out, I'm assuming I see something that isn't there.
One of the most powerful techniques for overcoming this problem is the Assert macro.
Assert Macro Many compilers offer an Assert() macro as part of their default library. The Assert() macro is designed to assert a fact about your program-that is, to document (and test) what you think is true at a given moment in the history of your program. Here's how it works: You assert something by passing it as an argument to the Assert macro. The macro takes no action if you are correct, but it aborts your program and puts up an error message if you are not correct-if the asserted "fact" is not true.
For example, you might write Assert(x>10). If x is greater than ten, then nothing happens, but if x is not greater than ten, the program halts and an error message is displayed. This is a great way to document your assumptions and also to find those pesky bugs that arise as a result of misunderstanding the state of your program.
The Assert macro is a powerful debugging tool, but for it to be acceptable in a professional development environment it must not create a performance penalty nor increase the size of the executable version of the program. To accomplish this, the preprocessor collapses the Assert macro into no code at all if Debug is not defined. Thus, in your development environment you can use Assert to find your bugs and misunderstandings, but when your code ships there is no penalty.
Rather than depending on the compiler-provided Assert(), you are free to write your own Assert() macro. Listing 1 illustrates a simple example.
On line , the term Debug is defined. Typically, this would be defined on the command line (or from within the Integrated Development Environment [IDE]) at compile time.
On lines 8-14, the Assert() macro is defined. Normally, this would be defined in a header file, and that header (assert.hpp) would be included in all your implementation files.
On line 5, the term Debug is tested. If it is not defined, Assert() is defined to create no code at all. If Debug is defined, the functionality defined on lines 8-14 is applied.
The Assert() itself is one long statement, split across seven source-code lines, as far as the precompiler is concerned. On line 9, the value passed in as a parameter is tested; if it evaluates false, the statements on lines 11-13 are invoked, printing an error message. If the value passed in evaluates true, no action is taken.
Assert vs. Exceptions The assert macro is not intended to handle runtime error conditions such as bad data, out-of-memory conditions, unable to open file, and so forth. Assert() is created to catch programming errors only. That is, if an Assert() "fires," you know you have a bug in your code.
This is critical, because when you ship your code to your customers, instances of Assert() will be removed. You can't depend on an Assert() to handle a runtime problem, because the Assert() won't be there.
Side Effects It is not uncommon to find that a bug appears only after the instances of Assert() are removed. This is almost always due to the program unintentionally depending on side effects of things done in Assert() and other debug-only code. For example, if you write "Assert (x=5)" when you mean to test whether x = 5, you will create a particularly nasty bug.
Let's say that just prior to this Assert() you called a function that set x equal to 0. With this Assert() you think you are testing whether x is equal to 5; actually, you are setting x equal to 5. The test returns TRUE, because x = 5 not only sets x to 5 but returns the value 5; and because 5 is non-0, it evaluates as TRUE.
Once you pass the Assert() statement, x really is equal to 5 (you just set it!). Your program runs just fine. You're ready to ship it, so you turn off debugging. Now the Assert() disappears, and you are no longer setting x to 5. Because x was set to 0 just before this, it remains at 0 and your program breaks.
In frustration, you turn debugging back on, but -presto!-the bug is gone. This is rather funny to watch, but not to live through, so be very careful about side effects in debugging code. If you see a bug that only appears when debugging is turned off, take a look at your debugging code and keep an eye out for nasty side effects.
Class Invariants Most classes have some conditions that should always be true. These class invariants are the sine qua non of your class. For example, it may be true that your Circle object should never have a radius of zero, or that your Animal should always have an age greater than 0 and less than 100.
It can be very helpful to declare an Invariants() method that returns true only if all of these conditions are true. You can then Assert(Invariants()) at the start and completion of every class method. The exception would be that your Invariants() would not expect to return true before your constructor runs or after your destructor ends.
Listing 2 shows a quick and dirty implementation of a String class and a trivial client class to illustrate how Invariants might be used. Please note, neither of these classes is complete nor industrial strength; I've kept them as simple as possible to illustrate how Invariants might be used.
On line 0 we define Debug so that our Assert macro will be in effect. On line 1 we defined Show_Invariants so that the program will write out each test of the class invariants. Line 6 takes the somewhat lazy expedient of using the standard namespace so that we don't have to qualify cout and other standard namespace identifiers.
Lines 8-19 recapitulate the definition of the Assert macro covered earlier. Beginning on line 21 we create a rudimentary String class. (I don't even try to create a complete implementation; if I needed a String class I'd use the one from the standard library.)
The important thing to note in this class declaration is on line 35, where the method Invariants is declared, returning a bool.
The constructors include an assertion that the class is in its valid state, in which all the invariant conditions are true, immediately after constructing the object. Note, again, that there is no such assertion at the start of the constructor. How could there be? After all, the job of the constructor is to create the object; we can't expect it to be valid before it is created. Similarly, the destructor begins by asserting the class invariants, but once the destructor is complete no such assurances are reasonable.
All the remaining methods begin and end with an assertion of the class Invariants. This says "I expect the object to be valid when I begin and when I end; along the way who knows what state the object will be in, but when I'm done it will be back to validity and I can assert that all the things which must always be true (that is, that are invariant) are true.
The actual implementation of the Invariants method is shown on lines 140-148. Here I check if Show_Invariants is defined; if so, I print out a message showing that the string invariants were validated. In normal use, there'd be no reason to print this, and the validation would be fast and silent. The actual test is quite simple: Either the string has a length, in which case the itsString pointer ought not to be null, or the length is 0, in which case the pointer ought to be null.
Line 151 creates a very simple test class: Animal, which is composed of a member variable, itsName, which is of type String. Animal declares its own Invariants method on line 180, and implements it on lines 193-200. This is very similar to String's Invariants, except that the invariant conditions tested are appropriate to this class (the Animal must have an age greater than 0, and its name must be at least 1 character long).
Finally, on lines 202-212 is a test driver method that creates an animal, prints out its age, and then sets its age to 8 and prints that. The output shows that the String Invariants is tested 7 times, the Animal is tested, the string is tested again, and then the Animal again before we even begin to print Sparky's age. Putting this in the debugger reveals that when Sparky is created a String is created out of the string constant ("Sparky"), and that causes the first call to String's Invariants method. The initialization of Sparky's name in the Animal constructor causes a call to the String copy constructor and another call to invariants.
Inside the copy constructor, the string offset operator calls (operator) repeatedly, and each call represents two calls to String Invariants. Once the Animal is created, the temporary string object is destroyed, and we see one more call to String Invariants at the start of the String destructor.
The call to GetName causes another call to String Invariants, as does the call to GetAge. The call to SetAge causes two calls. Why does GetAge cause only one, but SetAge two? In GetAge, shown on line 158, we have no work to do but to return a member variable, so we assert the invariants and return the variable. In SetAge, shown on lines 160-165, we do have work to do, so we bracket that work with Assertion statements.
Conclusion The judicious, perhaps even the fanatical, use of Asserts and Invariants can be instrumental in building code that is easier to maintain and has fewer bugs. It is possible to create a great variety of Assert-like macros, each designed to test various assumptions.