7.5. Tracing


Trace prints

Experienced software developers do all that they can during development to ensure that the software works. But they also know that, despite their efforts, there will be mistakes, and they plan for that from the start.

A common way to plan for mistakes is to add statements to a program that show what is going on in enough detail that mistakes can be detected and diagnosed. You want to turn on the lights rather than working on your software in the dark. Typically, trace statements write where a program is and what is in selected variables or data structures.

Once a unit is found to work, you do not remove the trace prints. Nor do you comment them out. After all, you might want to use them again.

Leave the trace prints in the program. When you add a trace print, surround it by a test such as

  if(tracing > 0)
  {
    ...
  }
where tracing is a global variable. If that variable is 0, traces are skipped. You turn them all on merely by setting tracing to a positive integer. Often you have levels of tracing, depending on how much detail you want to see. For example,
  if(tracing > 1)
  {
    ...
  }
says only to do this trace print for level 2 or higher.

You can also have more than one variable to control tracing. You typically have a variable for each module or collection of related modules. You can even test multiple variables. For example,

  if(tracing > 0 && traceQueueModule > 1)
  {
    ...
  }
only does the trace if tracing is turned on and traceQueueModule has level 2 or higher.


What a trace print should say

Never print raw numbers in a trace print. A trace print should normally contain the following information.

  1. Remember that there will, in general, be many trace prints in your software. Each one should say where it is by giving the name of the function (and possibly the module) in which it occurs. A typical way to do that in function insert is

      if(tracing > 0)
      {
        printf("insert: ...");
      }
    
    showing the name of the function as if you are writing a play and this is a line for it to speak.

  2. Say where this trace occurs in the function. Is it at the beginning? The end? At the top of a loop body?

  3. Provide relevant information. If you are showing the size of something, write "size = ...", where ... tells the size. Be sure to include important information. You will want to see it for diagnosing problems.

  4. Make traces readable. Do not run consecutive traces all onto one line. Put spaces between different pieces of information. Unreadable trace prints are worse than useless.


Adding a compile-time switch

Sometimes you want to leave trace prints in a a piece of software, but also want to be able to avoid the overhead of constantly checking if tracing is turned on, and do not want the trace code to enlarge your program. You typically do that for a version that is intended to be used by the customer rather that for development.

In a C++ program it is easy to get rid of trace prints automatically using conditional compilation. Either do or do not define a preprocessor symbol DEBUG. Then

  #ifdef DEBUG
    if(tracing > 0)
    {
      ...
    }
  #endif
allows you to avoid compiling the entire debug print, including the test of whether tracing > 0.


Avoiding clutter

An unfortunate drawback of trace prints is that they clutter function definitions, making the definitions more difficult to read. One way to avoid that is to move the trace prints out of the definition. Here is an example.

  #ifdef DEBUG
  #  define TRACE_INSERT1\
       if(tracing > 0)\
       {\
         printf("insert [top]: k = %i\n", k);\
       }
  #else
  #  define TRACE_INSERT1 {}
  #endif

  void insert(int k)
  {
    TRACE_INSERT1
    ... (body of insert)
  }
Now each trace print is replaced by one short line inside the function body, so it has minimal impact on the readability of the body.