8.6. Debugging Aids

 < Free Open Study > 

Another key aspect of defensive programming is the use of debugging aids, which can be a powerful ally in quickly detecting errors.

Don't Automatically Apply Production Constraints to the Development Version

A common programmer blind spot is the assumption that limitations of the production software apply to the development version. The production version has to run fast. The development version might be able to run slow. The production version has to be stingy with resources. The development version might be allowed to use resources extravagantly. The production version shouldn't expose dangerous operations to the user. The development version can have extra operations that you can use without a safety net.

Further Reading

For more on using debug code to support defensive programming, see Writing Solid Code (Maguire 1993).


One program I worked on made extensive use of a quadruply linked list. The linked-list code was error prone, and the linked list tended to get corrupted. I added a menu option to check the integrity of the linked list.

In debug mode, Microsoft Word contains code in the idle loop that checks the integrity of the Document object every few seconds. This helps to detect data corruption quickly, and it makes for easier error diagnosis.

Be willing to trade speed and resource usage during development in exchange for built-in tools that can make development go more smoothly.


Introduce Debugging Aids Early

The earlier you introduce debugging aids, the more they'll help. Typically, you won't go to the effort of writing a debugging aid until after you've been bitten by a problem several times. If you write the aid after the first time, however, or use one from a previous project, it will help throughout the project.

Use Offensive Programming

Exceptional cases should be handled in a way that makes them obvious during development and recoverable when production code is running. Michael Howard and David LeBlanc refer to this approach as "offensive programming" (Howard and LeBlanc 2003).

Cross-Reference

For more details on handling unanticipated cases, see "Tips for Using case Statements" in Section 15.2.


Suppose you have a case statement that you expect to handle only five kinds of events. During development, the default case should be used to generate a warning that says "Hey! There's another case here! Fix the program!" During production, however, the default case should do something more graceful, like writing a message to an error-log file.

Here are some ways you can program offensively:

A dead program normally does a lot less damage than a crippled one.

Andy Hunt and Dave Thoma

  • Make sure asserts abort the program. Don't allow programmers to get into the habit of just hitting the Enter key to bypass a known problem. Make the problem painful enough that it will be fixed.

  • Completely fill any memory allocated so that you can detect memory allocation errors.

  • Completely fill any files or streams allocated to flush out any file-format errors.

  • Be sure the code in each case statement's default or else clause fails hard (aborts the program) or is otherwise impossible to overlook.

  • Fill an object with junk data just before it's deleted.

  • Set up the program to e-mail error log files to yourself so that you can see the kinds of errors that are occurring in the released software, if that's appropriate for the kind of software you're developing.

Sometimes the best defense is a good offense. Fail hard during development so that you can fail softer during production.

Plan to Remove Debugging Aids

If you're writing code for your own use, it might be fine to leave all the debugging code in the program. If you're writing code for commercial use, the performance penalty in size and speed can be prohibitive. Plan to avoid shuffling debugging code in and out of a program. Here are several ways to do that:

Use version-control tools and build tools like ant and make Version-control tools can build different versions of a program from the same source files. In development mode, you can set the build tool to include all the debug code. In production mode, you can set it to exclude any debug code you don't want in the commercial version.

Cross-Reference

For details on version control, see Section 28.2, "Configuration Management."


Use a built-in preprocessor If your programming environment has a preprocessor as C++ does, for example you can include or exclude debug code at the flick of a compiler switch. You can use the preprocessor directly or by writing a macro that works with preprocessor definitions. Here's an example of writing code using the preprocessor directly:

C++ Example of Using the Preprocessor Directly to Control Debug Code
#define DEBUG       <-- 1 ... #if defined( DEBUG ) // debugging code ... #endif 

(1)To include the debugging code, use #define to define the symbol DEBUG. To exclude the debugging code, don't define DEBUG.

This theme has several variations. Rather than just defining DEBUG, you can assign it a value and then test for the value rather than testing whether it's defined. That way you can differentiate between different levels of debug code. You might have some debug code that you want in your program all the time, so you surround that by a statement like #if DEBUG > 0. Other debug code might be for specific purposes only, so you can surround it by a statement like #if DEBUG == POINTER_ERROR. In other places, you might want to set debug levels, so you could have statements like #if DEBUG > LEVEL_A.

If you don't like having #if defined()s spread throughout your code, you can write a preprocessor macro to accomplish the same task. Here's an example:

C++ Example of Using the Preprocessor Macro to Control Debug Code
 #define DEBUG #if defined( DEBUG ) #define DebugCode( code_fragment ) { code_fragment } #else #define DebugCode( code_fragment ) #endif ... DebugCode(    statement 1;       <-- 1    statement 2;         |    ...                  |    statement n;       <-- 1 ); ... 

(1)This code is included or excluded, depending on whether DEBUG has been defined.

As in the first example of using the preprocessor, this technique can be altered in a variety of ways that make it more sophisticated than completely including all debug code or completely excluding all of it.

Write your own preprocessor If a language doesn't include a preprocessor, it's fairly easy to write one for including and excluding debug code. Establish a convention for designating debug code, and write your precompiler to follow that convention. For example, in Java you could write a precompiler to respond to the keywords //#BEGIN DEBUG and //#END DEBUG. Write a script to call the preprocessor, and then compile the processed code. You'll save time in the long run, and you won't mistakenly compile the unpreprocessed code.

Cross-Reference

For more information on preprocessors and for direction to sources of information on writing one of your own, see "Macro Preprocessors" in Section 30.3.


Use debugging stubs In many instances, you can call a routine to do debugging checks. During development, the routine might perform several operations before control returns to the caller. For production code, you can replace the complicated routine with a stub routine that merely returns control immediately to the caller or that performs a couple of quick operations before returning control. This approach incurs only a small performance penalty, and it's a quicker solution than writing your own preprocessor. Keep both the development and production versions of the routines so that you can switch back and forth during future development and production.

Cross-Reference

For details on stubs, see "Building Scaf-folding to Test Individual Routines" in Section 22.5.


You might start with a routine designed to check pointers that are passed to it:

C++ Example of a Routine That Uses a Debugging Stub
 void DoSomething(    SOME_TYPE *pointer;    ...    ) {    // check parameters passed in    CheckPointer( pointer );       <-- 1    ... } 

(1)This line calls the routine to check the pointer.

During development, the CheckPointer() routine would perform full checking on the pointer. It would be slow but effective, and it could look like this:

C++ Example of a Routine for Checking Pointers During Development
 void CheckPointer( void *pointer ) {       <-- 1    // perform check 1--maybe check that it's not NULL    // perform check 2--maybe check that its dogtag is legitimate    // perform check 3--maybe check that what it points to isn't corrupted    ...    // perform check n--... } 

(1)This routine checks any pointer that's passed to it. It can be used during development to perform as many checks as you can bear.

When the code is ready for production, you might not want all the overhead associated with this pointer checking. You could swap out the preceding routine and swap in this routine:

C++ Example of a Routine for Checking Pointers During Production
 void CheckPointer( void *pointer ) {       <-- 1    // no code; just return to caller } 

(1)This routine just returns immediately to the caller.

This is not an exhaustive survey of all the ways you can plan to remove debugging aids, but it should be enough to give you an idea for some things that will work in your environment.

 < Free Open Study > 


Code Complete
Code Complete: A Practical Handbook of Software Construction, Second Edition
ISBN: 0735619670
EAN: 2147483647
Year: 2003
Pages: 334

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net