September 1998

Debugging with diagnostic macros

by Kent Reisdorph

Debugging is a never-ending process. Although it's often thought of as a process to find and eliminate bugs in your programs, that's only half of the story. Debugging is also a programming tool. This type of debugging might better be thought of as diagnostics, which tell you what your program is doing at any given moment. While breakpoints will tell you much of what you need to know, other diagnostic methods are also available. In this article, we'll discuss one such tool: diagnostic macros. We'll show you what they are and how to use them in your programs.

What macros?

The Borland C++ line of products offers a wide variety of diagnostic macros. (Some macros look and act much like functions, but are implemented differently.) Only two of these macros, however, are generally available in C++Builder: TRACE and WARN. These macros allow your program to output messages to a special file, as we'll explain in just a bit. For example, if you want to trace program execution from function to function, you can sprinkle calls to TRACE throughout your code. A call to TRACE looks like this:
TRACE("In my OnCreate handler...");
The text within quotes is sent to the diagnostic log file. Both TRACE and WARN let you use the iostream streaming operators within the macro. For example, to output the value of a particular variable to the diagnostic log file, you could write code like the following:
TRACE("Entering OnFormCreate handler...");
TRACE("ParamCount = " << ParamCount());
TRACE("ParamStr(0) = " << ParamStr(0));
In this case, the value of the ParamCount() function is output followed by the value of ParamCount(0). (The ParamCount() function returns the number of command-line parameters, and ParamStr() returns an individual command line parameter. ParamStr(0) always returns the path and filename of the executable.) The previous code snippet would result in the following entry to the diagnostic log file:
Trace Unit1.cpp 20: [Def] 
  Entering OnFormCreate handler...

Trace Unit1.cpp 21: [Def] ParamCount = 0

Trace Unit1.cpp 22: [Def] 
  ParamStr(0) = E:\Project1.exe
We split some of the lines here--the text would all appear on a single line in the log file. We'll talk in just a minute about the format of the log file and how to use it. The WARN macro works like the TRACE, but WARN macro lets you test for a condition as well as send a text string to the log file. If the condition is met, the text is sent to the diagnostic log file; if the condition isn't met, then the text isn't sent. Here's an example:

 

WARN(Handle == 0, 
	"Something went wrong...");
The value of Handle is checked for a non-zero value. If Handle is 0, then the message is sent to the log file; if Handle contains a valid value, then nothing is written to the log file. WARN allows you to output only the messages that you want to see for a given condition. This helps to eliminate clutter in the log file so you can easily find the information you're looking for.

The log file: OutDbg1.txt

You may be thinking, "So the diagnostic macros send text messages to a log file. But what is log file and how do I use it?" The diagnostic macros send output to a special file called OUTDBG1.TXT. It's special for a couple of reasons. First, any time TRACE or WARN messages are generated, OUTDBG1.TXT automatically appears as a new file in the C++Builder Code Editor, as shown in Figure A.

Figure A: When a message is generated, the OUTDBG1.TXT file appears in the Code Editor.
[ Figure A ]

Second, OUTDBG1.TXT isn't saved as part of your project when you use the Save or Save All command. It's treated as a temporary file, and its contents are discarded when you close the file or the project. In addition, you'll never be prompted to save OUTDBG1.TXT. You can explicitly save the log file with File | Save, if you wish.

 
Note: DLL messages
You may have encountered OUTDBG1.TXT from time to time as you've worked with C++Builder. Once in a while, you'll see a message that warns of a DLL collision. This message is just for informational purposes. The warning simply tells you that the DLL couldn't be loaded at the requested memory location and therefore was loaded in another place in memory. You can happily ignore such messages.

The messages sent to OUTDBG1.TXT are appended to any text already in the file. In other words, if you run your program repeatedly, the contents of OUTDBG1.TXT will grow--old message aren't removed, and new TRACE and WARN messages are added to the end of the file.

 

Getting ready

In order to get the TRACE and WARN macros to work, you need to perform two steps:
bullet Include the CHECKS.H header.
bullet Define __TRACE and __WARN in your application.

Begin by including CHECKS.H in any units that use the TRACE and WARN macros, as follows:

#include <checks.h>
If you forget to include this file, you'll get a compiler error about TRACE or WARN being undefined symbols. Next, define the __TRACE and __WARN symbols. If you fail to define these symbols, the TRACE and WARN macros won't work. You won't see any indication that the macros aren't working--you simply won't get any output in OUTDBG1.TXT.

You can define these symbols two ways. First, you can define them in one of your source code units or headers:

 

#define __TRACE
#define __WARN
Note that each symbol has two underscores preceeding the symbol name. An arguably better way to define the __TRACE and __WARN symbols is to add them to the Conditional Defines field in the Project Options dialog box. To do this, open the Project Options dialog box, then move to the Directories/ Conditionals page. Enter the following text in the Conditional Defines field:

 

__TRACE;__WARN
Again, be sure you put a double underscore before each symbol. Notice that a semicolon separates the symbols and that there are no spaces between them. Once you've performed these steps, you can begin to put the TRACE and WARN macros to work. Listing A contains a short program that illustrates how to use these C++Builder macros. The program consists of a form with an edit control and a button.

Listing A: DIAGSU.CPP

#define __TRACE
#define __WARN
//----------------------------
	-----------------
#include <vcl\vcl.h>
#include <checks.h>
#pragma hdrstop

#include "DiagsU.h"
//----------------------------
	-----------------
#pragma resource "*.dfm"
TForm1 *Form1;
//----------------------------
	-----------------__fastcall 
		TForm1::TForm1
			(TComponent* Owner)
  : TForm(Owner)
{
}
//--------------------------
	-------------------
void __fastcall TForm1::
	FormCreate(TObject *Sender)
{
  TRACE("Entering OnForm
	Create handler...");
  TRACE("ParamCount = " 
	<< ParamCount());
  TRACE("ParamStr(0) = " 
	<< ParamStr(0));
}
//--------------------------
	-------------------
void __fastcall TForm1::
			Button1Click
			(TObject *Sender)
{
  TRACE("In OnClick handler
	 for " <<
    dynamic_cast<TComponent*
	>(Sender)->Name
    << "...");
  static int count;
  count++;
  // Output message if button has been
  // clicked more than 5 times.
  WARN(count > 5, "Button has 
	been clicked "
    << count << " times.")
  TRACE("Edit1 contains: " << 
    Edit1->Text.c_str());
}
//----------------------------
	-----------------void __fastcall 
		TForm1::FormClose(TObject
		 *Sender, TCloseAction 
			&Action)
{
  TRACE("Entering OnFormClose handler...");
  TRACE("Application closing...");
}

Conclusion

A good programmer will use every available resource when developing and debugging applications. Add the TRACE and WARN macros to your debugging arsenal, and you'll be a more productive programmer.