© 2016 Fritz Sieker
Two common techniques for debugging are adding printf() calls to your code and using debuggers. The methods are complimentary techniques that have different strengths and weaknesses. To be a good programmer, you must be familiar with both.
Advantages of debuggers:
Advantages of printf() debugging
Using printf() for debugging is a subset of the general problem of logging. Logging frameworks provide substantial capabilities and flexibility. This page provides simple mechanisms that are sufficient for many student programs.
But, inserting printf() code in your program has a major disadvantage. The only way to get rid of the output is to remove the code or comment it out. If you remove it, you have lost something that you put effort into, and which might be useful in the future. If you comment it out, you may create bugs if you comment out too much/little. And to turn it back on, you must un-comment it. Of course, C and C++ users may use conditional compilation. An ideal solution to printf() style debugging would have the following characteristics:
The debug module should be initialized by calling the appropriate version of the init() function. This should be the first line in main. If the first command line argument is, or starts with, -debug the class is initialized and the first parameter is removed from the args. The first parameter may have one of three forms:
The other three tabs present printf() style debugging "helpers". These were created after lots of reading on the web (particularly stackoverflow.com). The code is my attempt to provide code students may use in debugging their programming assignments. The primary goal of these implementations was simplicity, with the other goals addressed consistent with an easy/short solution that they could read and study.
All of the solutions presented here have the characteristics described above. Each prints its output to the error stream (System.err, stderr, cerr). This allows the users to take advantage of output redirection to capture the output. Alternatively, the file may be specified. Each also prefaces the output with "DEBUG" to allow convenient filtering with tools like grep.
The Java solution relies on a single class Debug.java. This is the javadoc for the class. The two major methods are Debug.printf() and Debug.format(). The first is identical to the System.out.printf() method except that the print is wrapped in a condition to check whether debugging is turned on. It also calls Debug.format() before printing and automatically adds a newline after the output. The Debug.format() method is an extension of String.format() that formats all array parameters using Arrays.toString(), thus relieving the user of needing to do it to see the values.
Since this relies on a method call and the evaluation of parameters, it has a code size and performance impact. To minimize performance hits, the Debug.printf() always returns true. This means that an assert may be placed before the call and the JVM will do a fast check before doing any other work. The JVM may choose to optimize out all the calls thus eliminating the memory size and performance implications of its use. This is an "abuse" of the assert capability of java but works well here. For more information on java assertions see this discussion.
To use Debug with you code, simply copy the Debug.java file into your code base. For simplicity, the code is not part of a package.
The short program DemoDebug.java demonstrates how to use some of the features of Debug.java.
1 public class DemoDebug { 2 3 private static void foo() { 4 Debug.HERE(); 5 } 6 7 public static void main (String[] args) { 8 args = Debug.init(args); // set debugLevel, reset args 9 10 System.out.println("Hello world"); 11 foo(); 12 13 Debug.printf("called with %d args %s", args.length, args); 14 Debug.printf(2, "level 2 debug output"); 15 16 // this is only printed if asserts enabled AND debug turned on 17 assert Debug.printf("asserted args: %s", (Object) args); // demo req'd cast 18 assert Debug.printf(2, "asserted level 2 debug output"); 19 20 Debug.close(); // optional unless sending output to file, may want to 21 // put this in a Java finally clause to guarantee close 22 } 23 }
Running the program in various ways produces the following outputs. Note that the call to Debug.init() checks for the the debug switch in the first parameter and sets DEBUG.debugLevel as appropriate. The call is not required, but the user must then set Debug.debugLevel with other code.
$: javac DemoDebug.java Debug.java
$: java DemoDebug
Hello world
$: java DemoDebug -debug foo
Hello world
DEBUG Test.java[4] foo() HERE
DEBUG Test.java[13] main() called with 1 args [foo]
$: java -ea DemoDebug -debug2 foo bar
Hello world
DEBUG DemoDebug.java[4] foo() HERE
DEBUG DemoDebug.java[13] main() called with 2 args [foo, bar]
DEBUG DemoDebug.java[14] main() level 2 debug output
DEBUG DemoDebug.java[17] main() asserted args: [foo, bar]
DEBUG DemoDebug.java[18] main() asserted level 2 debug output
main()
must be a call to
Debug.init()
, define a shortcut to create the call. The general
sequence for creating an Eclipse shortcut is:
templates
Java/Editor/Templates
and select it.New
to create a new shortcut.New Template
dialog, give the shortcut a name. This
will be the character sequence you type to invoke the shortcut. Give it a
brief description. And finally, add what you want the shortcut to generate.
OK
to complete the definition.[Ctrl] + [space]
.
The two shortcuts you might find useful is to define dbi
(a
shortcut to the Debug.init()
call) as:
${enclosing_method_arguments} = Debug.init(${enclosing_method_arguments});${cursor}
and dbg
(a shortcut to Debug.printf()
) as
Debug.printf("${cursor}");
The C implementation relies on the C preprocessor. A very simple implementation for debugging is a single header file:
#ifndef __DEBUG__ #define __DEBUG__ #idfef DEBUG #define debug(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__) #else #define debug(fmt, ...) ((void)0) #endif #endif
This implementation compiles the code replacing debug() by fprintf() or by an empty statement that has no effect. This is easy, and has no runtime size/performance penalties, but requires recompile to turn debugging on/off. It does not provide varying the amount of output at runtime nor does it do syntax checking when not enabled. To use it, simply include the header in your code and add the flag -DDEBUG to your compile. The code ##__VA_ARGS__ is a gcc extension.
The following implementation addresses these limitations. Note that the debug code is always compiled. This makes sure the debug code is not out of sync with the main code. Instead, of "eliminating" the code (as in the first example), it relies on the compiler optimizing out the debug code. For convenience, it adds several other handy debugging macros. It also takes advantage of the preprocessor and compiler to add useful information to every line. The macros use the ability to concatenate adjacent literal strings and to stringify macro variables. Based on code and ideas found here and here.
The implementation consists of two files: a header file Debug.h and a small source file Debug.c. The function debugInit() initializes the module and must be called before any debug code is executed. When compiling the code a -DDEBUG must be part of the compile. Consider the following file test.c that demonstrates various features of the implementation.
1 #include <stdio.h>
2
3 #include "Debug.h"
4
5 void foo (void) {
6 HERE;
7 }
8
9 int main (int argc, const char* argv[]) {
10 debugInit(&argc, argv);
11
12 printf("Hello world\n");
13 foo();
14
15 debug("Simple debug");
16 lDebug(2, "level 2 %s=(%d)", debugV(argc));
17 vDebug("%s", argv[0]);
18
19 if (argc > 1)
20 vDebug("%d", argc);
21 else
22 printf("no arguments\n");
23 }
Running the program in various ways produces the following outputs:
$: gcc -DDEBUG -Wall test.c Debug.c $: a.out no arguments $:a.out -debug Hello world DEBUG test.c[6] foo() HERE DEBUG test.c[15] main() Simple debug DEBUG test.c[17] main() argv[0]=(a.out) no arguments $:a.out -debug2 foo Hello world DEBUG test.c[6] foo() HERE DEBUG test.c[15] main() Simple debug DEBUG test.c[16] main() level 2 argc=(2) DEBUG test.c[17] main() argv[0]=(a.out) DEBUG test.c[20] main() argc=(2)
The main macro from Debug.h defines lDebug
#define lDebug(level, fmt, ...) \ do { \ if (DEBUG_ENABLED && (debugLevel >= level)) \ fprintf((debugFile ? debugFile : stderr), "DEBUG %s[%d] %s() " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__); \ } while(0)
This macro takes a variable number of parameters. The first is an integer which defines at what point the macro should actually print the message. The second is a format string which is identical to the format string of fprintf(). The remaining parameters are just the values that are printed by the format string. The fmt string MUST be a string literal. The macro expands into an expression consisting of three parts:
The macro vDebug illustrates the ability of the C preprocessor to "stringify" a parameter. This is the construct #name.
#define debugV(name) #name,(name) #define vDebug(fmt, name) debug("%s=(" fmt ")" , debugV(name))
The C++ implementation presents a stream model of output utilizing the << operator. The implementation consists of two files: Debug.h and Debug.cpp. The method Debug::init() must be called before any of the macros are used. Like the C implementation, this relies on the C preprocessor to determine the file name, line number and function name. Consider the following file test.cpp that demonstrates various features of the implementation.
1 #include <iostream> 2 #include <fstream> 3 4 #include "Debug.h" 5 6 void foo (void) { 7 HERE; 8 } 9 10 int main (int argc, const char* argv[]) { 11 Debug::init(argc, argv); // optional way of initializing module 12 13 std::cout << "Hello world" << std::endl; 14 foo(); 15 16 debug << "Simple debug"; 17 lDebug(2) << "level 2 " << debugV(argc); 18 vDebug(argv[0]); 19 20 if (argc > 1) 21 debug << debugV(argc) << debugV(argv[1]); 22 else 23 std::cout << "no arguments" << std::endl; 24 }
Running the program in various ways produces the following output. Note that the -debug is removed from the argument list by the Debug::init() call, so that the program "sees" fewer arguments that were actually passed.
$: g++ -DDEBUG -Wall Debug.cpp test.cpp $: a.out Hello world no arguments $: a.out -debug Hello world DEBUG test.cpp[7] foo() HERE DEBUG test.cpp[16] main() Simple debug DEBUG test.cpp[18] main() argv[0]=(a.out) no arguments $: a.out -debug2 foo Hello world DEBUG test.cpp[7] foo() HERE DEBUG test.cpp[16] main() Simple debug DEBUG test.cpp[17] main() level 2 argc=(2) DEBUG test.cpp[18] main() argv[0]=(a.out) DEBUG test.cpp[21] main() argc=(2) argv[1]=(foo)
The implementation relies on expanding the debug macro lDebug() into an if/else statement that control whether the debug output is printed or not. The true portion of the statement is an empty statement that is optimized out. The else contains the actual debug statement. In the else clause, an anonymous instance of the Debug class is created. This starts the output of the debugging. The class overloads the << operator. The instance is destructed at the end of the statement. The if/else is used instead of a plain if to avoid a dangling else problem. The major macro from Debug.h is given below. All the other macros expand into a use of lDebug(). The example code shows how debug may be treated as if it were a stream.
#define lDebug(lvl) \ if ((! DEBUG_ENABLED) || ((lvl) > Debug::debugLevel)) \ ; \ else \ Debug(__FILE__, __LINE__, __func__)
Note that Debug::dbOS is a pointer, not an ostream. See this discussion for details on why this is a pointer.