MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Debugging hints

In an ideal environment, you have an excellent debugger available that easily makes the behavior of your program transparent so you can quickly discover errors. However, most debuggers have blind spots, and these will require you to embed code snippets in your program to help you understand what’s going on. In addition, you may be developing in an environment (such as an embedded system, which is where I spent my formative years) that has no debugger available, and perhaps very limited feedback (i.e. a one-line LED display). In these cases you become creative in the ways you discover and display information about the execution of your program. This section suggests some techniques for doing this.

Debugging flags

If you hard-wire your debugging code into a program, you can run into problems. You start to get too much information, which makes the bugs difficult to isolate. When you think you’ve found the bug you start tearing out debugging code, only to find you need to put it back in again. You can solve these problems with two types of flags: preprocessor debugging flags and run-time debugging flags.

Preprocessor debugging flags

By using the preprocessor to #define one or more debugging flags (preferably in a header file), you can test a flag using an #ifdef statement and conditionally include debugging code. When you think your debugging is finished, you can simply #undef the flag(s) and the code will automatically be removed (and you’ll reduce the size and run-time overhead of your executable file).

It is best to decide on names for debugging flags before you begin building your project so the names will be consistent. Preprocessor flags are traditionally distinguished from variables by writing them in all upper case. A common flag name is simply DEBUG (but be careful you don’t use NDEBUG, which is reserved in C). The sequence of statements might be:

#define DEBUG // Probably in a header file
//...
#ifdef DEBUG // Check to see if flag is defined
/* debugging code here */
#endif // DEBUG 

Most C and C++ implementations will also let you #define and #undef flags from the compiler command line, so you can re-compile code and insert debugging information with a single command (preferably via the makefile, a tool that will be described next). Check your local documentation for details.

Run-time debugging flags

In some situations it is more convenient to turn debugging flags on and off during program execution, especially by setting them when the program starts up using the command line. Large programs are tedious to recompile just to insert debugging code.

To turn the debugger on and off dynamically, create bool flags:

//: C03:DynamicDebugFlags.cpp
#include <iostream>
#include <string>
using namespace std;
// Debug flags aren't necessarily global:
bool debug = false;

int main(int argc, char* argv[]) {
  for(int i = 0; i < argc; i++)
    if(string(argv[i]) == "--debug=on")
      debug = true;
  bool go = true;
  while(go) {
    if(debug) {
      // Debugging code here
      cout << "Debugger is now on!" << endl;
    } else {
      cout << "Debugger is now off." << endl;
    }  
    cout << "Turn debugger [on/off/quit]: ";
    string reply;
    cin >> reply;
    if(reply == "on") debug = true; // Turn it on
    if(reply == "off") debug = false; // Off
    if(reply == "quit") break; // Out of 'while'
  }
} ///:~ 

This program continues to allow you to turn the debugging flag on and off until you type “quit” to tell it you want to exit. Notice that it requires that full words be typed in, not just letters (you can shorten it to letter if you wish). Also, a command-line argument can optionally be used to turn debugging on a startup – this argument can appear anyplace in the command line, since the startup code in main( ) looks at all the arguments. The testing is quite simple because of the expression:

string(argv[i])

This takes the argv[i] character array and creates a string, which then can be easily compared to the right-hand side of the ==. The above program searches for the entire string --debug=on. You can also look for --debug= and then see what’s after that, to provide more options. The chapter on the string class later in the book will show you how to do that.

Although a debugging flag is one of the relatively few areas where it makes a lot of sense to use a global variable, there’s nothing that says it must be that way. Notice that the variable is in lower case letters to remind the reader it isn’t a preprocessor flag.

Turning variables and expressions into strings

When writing debugging code, it is tedious to write print expressions consisting of a character array containing the variable name, followed by the variable. Fortunately, Standard C includes the stringize operator ‘ #’, which was used earlier in this chapter. When you put a # before an argument in a preprocessor macro, the preprocessor turns that argument into a character array. This, combined with the fact that character arrays with no intervening punctuation are concatenated into a single character array, allows you to make a very convenient macro for printing the values of variables during debugging:

#define PR(x) cout << #x " = " << x << "\n";

If you print the variable a by calling the macro PR(a), it will have the same effect as the code:

cout << "a = " << a << "\n";

This same process works with entire expressions. The following program uses a macro to create a shorthand that prints the stringized expression and then evaluates the expression and prints the result:

//: C03:StringizingExpressions.cpp
#include <iostream>
using namespace std;

#define P(A) cout << #A << ": " << (A) << endl;

int main() {
  int a = 1, b = 2, c = 3;
  P(a); P(b); P(c);
  P(a + b);
  P((c - a)/b);
} ///:~ 

You can see how a technique like this can quickly become indispensible, especially if you have no debugger (or must use multiple development environments). You can also insert an #ifdef to cause P(A) to be defined as “nothing” when you want to strip out debugging.

The C assert( ) macro

In the standard header file <cassert> you’ll find assert( ), which is a convenient debugging macro. When you use assert( ), you give it an argument that is an expression you are “asserting to be true.” The preprocessor generates code that will test the assertion. If the assertion isn’t true, the program will stop after issuing an error message telling you what the assertion was and that it failed. Here’s a trivial example:

//: C03:Assert.cpp
// Use of the assert() debugging macro
#include <cassert>  // Contains the macro
using namespace std;

int main() {
  int i = 100;
  assert(i != 100);
} ///:~ 

The macro originated in Standard C, so it’s also available in the header file assert.h.

When you are finished debugging, you can remove the code generated by the macro by placing the line:

#define NDEBUG

in the program before the inclusion of <cassert>, or by defining NDEBUG on the compiler command line. NDEBUG is a flag used in <cassert> to change the way code is generated by the macros.

Later in this book, you’ll see some more sophisticated alternatives to assert( ) .

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru