Debugging/Testing


A New Trace Class

Puneesh Chaudhry

Notation isn't everything, but a judicious use of operator overloading can make your C++ trace statements both easier to read and safer to compile.


Introduction

Debugging of programs using trace statements is an age-old technique. Unfortunately the methods employed for tracing are also quite antiquated. The traditional method of using a function with a varying number of arguments (such as printf) has a variety of drawbacks, ranging from lack of type safety (due to format strings) to an inability to specify objects of user-defined data types for tracing.

I present here a new method (pun unintended). It not only overcomes the problems associated with the printf family of functions, but also is very easy to use. This method exploits the overloading of the function operator() to advantage. Specifically, if a class X has a member function with the signature:

int operator() (int);

then you can write:

X a;
int i = a(10);

to "call" the object as if it were a function.

Class Trace

Let's define a class Trace which overloads the operator() function in the following way:

void  operator() (const char*,
    const Packet &);
void  operator() (const char*,
    int iVal);

With this class definition you can write:

Trace gTrace;
Packet sendPkt;
Packet recvPkt;
int  numPkt;
.....
gTrace("sendPacket=", sendPkt);
gTrace("recvPacket=", recvPkt);
gTrace("num pkts =", numPkt);

As you can see, with this approach you can specify variables of both standard as well as user-defined data types for tracing, without having to specify any format strings. This approach still does not allow you to specify varying number of arguments, however. So let's modify the overloaded operator() functions slightly as follows:

const Trace& operator() (const char*,
    const Packet &) const;
const Trace& operator() (const char*,
    int iVal) const;

Now all the operator() functions return a const reference to the Trace class itself. The functions themselves have also been defined as const. This permits an interesting notation called "chaining." You can now combine the three trace statements above into one, and add any number of additional arguments as follows:

gTrace("sendPacket=", sendPkt)
    ("recvPacket=", recvPkt)
    ("num pkts =", numPkt);

Since each operator() function returns a reference to the original object, the next operator() function can be called directly after it, making a chain of function calls. This way you can go on adding arguments for tracing indefinitely; and instead of taxing yourself to provide the correct format strings for each variable, you can rely on your compiler to call the correct operator() function. See Listing 1 for a more elaborate example.

Apart from ease of use, this approach has the following advantages:

1) It provides a type-safe way of specifying a varying number of arguments, by doing away with format strings, which are clumsy to use and error prone.

2) It allows easy dumping of variations of data types like multi-dimensional arrays, arrays of strings etc. (See Listing 1) .

3) Both standard as well as user defined conversions can be used, such as from char to int (standard conversion) and from String to char* (a very common user-defined conversion). o

Puneesh Chaudhry is 22 years old, and he has a B.E. in Computer Science from Delhi College of Engineering. He is currently a programmer for River Run Software Group, of Durham, NC. His interests include networking and the Internet. He can be reached at puneesh@riverrun.com.