Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Object-Oriented Debugging


NOV90: OBJECT-ORIENTED DEBUGGING

Strategies and tools for debugging your OOP apps

Simon is a member of SCO Canada's C++ development tool project and can be contacted at 130 Bloor Street West, 10th Floor, Toronto, Ontario, Canada M5S 1N5.


This article discusses strategies and tools for object-oriented debugging. Because of its facilities for data abstraction and inheritance, and because of its similarities to C, the language used to demonstrate the principles in this article will be C++, and a good portion of the content will be C++ specific. However, many of the concepts presented here are applicable to other object-oriented environments.

C++ Debugging is Different

Typical C applications tend to be procedure oriented. Execution flows from point A to point B, and somewhere in between, a function may be called to manipulate data, read it in, or write it out. Functions may have side-effects involving many pieces of data or none. When you debug, you watch statement execution to ensure that everything happens in expected order.

In a well-designed C++ application, the "methods" (member functions) available to manipulate an item of data are well defined. Access to data is restricted via a clean set of interface routines. Because of this, the state of the data is very important and is what we are typically concerned with when verifying the correctness of C++ code. Thus, intelligent access to data is as important as program execution flow when searching for a problem. Designing special debugging routines from the beginning can make accessing that data less of a headache for the programmer.

C++ compilers enforce much stricter type checking than traditional C compilers. This all but eliminates several problems such as calling functions with the wrong arguments, type mismatches in assignments, and attempts to store data into constant objects. There are far more original ways to shoot yourself in the foot these days.

A Sample Class

For examples presented in this article, I'll use a simple string class. Several related classes, such as a class containing a list of strings and a class that iterates over a list of strings, are presented. These classes are declared in Listing One (page 114); the main( ) program is in Listing Two (page 114).

Many of the functions will be trivial when applied against these small classes; it is not my intent in this article to advocate bogging down a 20-line class with several hundred lines of extra debugging code, but to demonstrate practices that are useful when dealing with more complex classes.

The String class provides us with a container for simple text strings. Storage allocation for buffers is performed via malloc( ), and the internal pointer s will be guaranteed to always point at a null-terminated string of length 0 or greater.

A StringList is an ordered container for Strings. This class is implemented as a singly linked list of StringListElements, each of which points to a String and to the next element in the list.

By declaring an instance of a StringList-Iterator, a method is provided to examine the individual entries in a StringList without having any knowledge of the internals of StringList.

Debug From the Start

Too often, debugging is left as the final stage of development, because problems only become evident after a program is compiled and run for the first time. Think about debugging while designing data structures. This forces you to ask questions such as "How can I prove that data is correct?" "How does this function modify the data?" and "What assumptions are being made here?" at a point where something can be done about weaknesses in design.

You can usually afford to be inefficient with debugging code, but you must write carefully; incorrectly flagging an error can lead to hard-to-find problems. At the same time, debugging libraries and user debug code are also prone to errors. I once spent four days tracking a problem in my application because a beta version of a debugging malloc( ) library incorrectly told me I had wild data pointers -- and then had the gall to abort my program.

Using Assert Macros

Long familiar to C users, the assert( ) macro (which is trivial to write, but is supplied by most compiler vendors) finds a good home in C++. This macro (defined, in Listings Three and Four, page 114) takes an expression as a parameter, and, if the expression is false (has the value 0), usually halts the program and prints a message giving the line number and file where the problem occurred. This tendency to abort means that a program with assert( ) scattered throughout is less resilient, if that program is not fully debugged.

At any point where an important assumption is made about the correctness of calculated data, insert an assert( ) call, which will display a message if that assumption is invalid. assert( ) macros should not be used to test input data for correctness; that always belongs within the program proper because it is a likely occurrence. If a switch statement doesn't have a default: case, add one, wrap it in #ifdef DEBUG, and add an assert( ) within it. For portability, never include character strings within the condition; many implementations do not handle this properly.

In Figure 1, StringListIterator::reset( ) (a function that restarts the iterator instance on the current class) requires a pointer to the StringList to be iterated over. The first assert( ) will ensure that the current instance is valid before we start operating on it. The second assert( ) ensures that this instance already points to a StringList before trying to access the beginning of that list. Although an instance of StringListIterator may be created without a list to iterate over, it is an error to perform a reset( ) or a next( ) without previously pointing to a StringList.

Figure 1: Using the assert( ) macro

  //   reset( ) - rewind iterator to first element of list
  void StringListlterator::reset(void)

  {
          //ensure valid data
          assert(verify( ));
          //we cannot reset if no list
          assert(list !=NULL);
          //point to beginning of list
          nxt = list->head;
  }

assert( ) is a macro, and because of this it can vanish when a program is compiled for production use. All code that is added for debugging only should be wrapped in preprocessor #ifdef DEBUG/#endif directives so that it vanishes in production programs when DEBUG is not defined.

Nevertheless, it may be desirable to always compile assert( )s into a program to ensure that problems in the field are properly detected. Yet often performance, speed, and robustness considerations outweigh this desire. A program that keeps running may be more useful to a user than one that aborts because an assert( ) for a trivial (or worse, nonexistent) error has been triggered.

Verifying Data Structures

When debugging a live program, it is quite easy to display an integer, string, or other simple data structure, look at it, and say "Yes, this looks good." Verification of more complex structures may involve several layers of pointer indirection. It may be tedious to examine all elements in, say, a linked list by hand, following all the pointers through until the end.

If you write functions to check your structures, these can be called within your program and directly by you at debug time. For instances of classes, these should be virtual member functions so that the correct routine is called for instances of derived classes accessed through base class pointers. Call these functions at the start and end of all other member functions to flag corrupted data and assist in isolating the problem.

If an assert fails at the start of the function, some agent external to the class was able to modify the data. If an assert fails at the end of the function, the function itself incorrectly modified the data. These functions are useful both combined with assert( ) macros and when called from the debugger. An example of verify( ) for the StringListIterator can be found in Listing Five (page 114).

Displaying Data Structures

Examining a complex structure by hand can be tedious in exactly the same way that verifying that structure is. The structure may be complex, involve multiple pointer indirections, or simply be so big that only a small portion is actually useful. It is therefore advisable to add functions to display data structures in some concise fashion. Again, within a class hierarchy these should be virtual member functions. These may never be called by application code, but are useful when called by the debugger. The dump( ) function that displays data for StringList is also found in Listing Five.

Data verification and data display routines should be named consistently. I chose verify( ) and dump( ), so that I always know what to call for any given structure.

Use Debugging Libraries

Since data is often the primary concern of a C++ program, it follows that many problems may surround the functions of memory allocation and deletion. Linking with one of the many readily available debugging malloc( ) libraries allows runtime checks to ensure that nothing runs past the end of a malloc'd area and that only valid pointers are passed to free( ).

In a C++ program, one has the option of overloading new and delete on a class-by-class basis to gather statistics, check for potential problems, or find "memory leaks." Depending on the compiler implementation, new and delete often eventually call malloc( ) and free( ), in which case a debugging malloc( ) library is useful here, too.

Deep and Shallow Copy

Many C++ data structures differentiate between "deep" and "shallow" copies. A deep copy copies the structure itself and any items that pointers in the structure point to. An example of a deep copy is String::operator= in the example classes. Deep copies ensure independence between different instances of a class by producing an entirely new copy of a structure. Shallow copies usually copy the structure itself only, but any pointers are left pointing at their original targets.

Shallow copies are quick and good for making a read-only copy of an object. Since the shallow copy does not have control over the items it points to, care must be taken not to destroy those objects during the normal use of either the original or the copy. Often, items pointed to by objects that implement shallow copies contain reference counts, so they are only destroyed when they are no longer required by any object. The Stroustrup book contains a good example of a string class implemented using reference counts. If reference counts are used, the shallow copy is usually as good as a deep copy.

One typical problem involves destroying the original object after a shallow copy has been made. The pointers within the copy end up pointing at free storage, which may "look" perfectly normal until it is reused, some time later. A related (and usually identical) problem arises if the copy is destroyed; then the original may be invalidated.

Memory Leaks

A common mistake in writing an overloaded operator is to allocate a new instance of a class, calculate the new value of this instance, and return the pointer to this new instance. This will work, but the compiler has no way of knowing that it should free up the new data after it has finished with it. A program may work perfectly well on small amounts of data but crash after it has used most of available memory. This is called a "memory leak." The code fragment in Figure 2 illustrates one such case.

Figure 2: Hidden memory leak

  Thing & operator + (Thing& one, Thing& two)
  {
           Thing*p = new Thing;
           //. . . .perform some magic . . .
           return *p;
  }

Multitasking systems such as Unix allow you to take a suspect section of code and loop through it again and again while watching the size of the process grow and grow, but DOS does not easily allow this. With DOS, a good debugging malloc( ) library is (again) useful. Most debugging malloc( )s can produce a log of malloc( ) and free( ) calls which can then be examined for problems.

Trivial Casting

Casting a pointer from an instance of a derived class to a base class does not change the virtual functions associated with that instance. In particular, the example in Figure 3 will loop forever if derivedClass::dump( ) is called. Since baseClass::dump( ) is accessed through the virtual function table (often referred to as the "vtbl" due to one implementation of this feature), and the virtual function entry in an instance of a derived class points to derived Class::dump( ), the statement ((baseClass *)(this)->dump( ) will simply call derivedClass::dump( ) recursively. In this case, a proper call to the desired function would use the syntax baseClass::dump( ).

Figure 3: Incorrect use of pointer casting

  struct baseClass
  {
           int i;
           virtual void dump( )
           {
                   cerr <<"i=" <<i<<"\n";
           }
  };
  struct derivedClass : baseClass
  {
           int j;
           virtual void dump( )
           {
                         //The line below is wrong:
                         ((baseClass *)(this))->dump( );

                         cerr <<"j=" <<j<<"\n";
           }
  };

In Figure 4 (ignoring the fact that this example is contrived and is poor coding practice), the compiler silently creates a temporary variable of type Class1 and passes a reference to this temporary into func( ). When func( ) returns, the modified temporary is deleted and the value of the real ptr->i is unchanged. This all happens silently in most C++ compilers, and is a source not only of inefficiency, but also errors. Looking at the C output (if available) confirms this, but it is a hard problem to catch unless you suspect it exists.

Figure 4: Invisible (and silent) temporary created by a cast

  struct Class1 {int i;};
  struct Class2 {int i;};

  void func(Class1& ref)
  {
          ref.i = 5;
  }
  main( )
  {
            Class2*ptr = new Class2;
            ptr->i = 1;
            cerr<< "before, i=" << (ptr->i)<<"\n";
            func((Class1)(*ptr));
            cerr << "after, i=" << (ptr->) <<"\n";
  }

Using a Debugger

Use a good debugger to help to know your program. Be aware of its capabilities and expand them through the addition of debugging routines where it falls short.

Most C debugging sessions consist of stack traces, setting simple break-points, and single-stepping. Since C++ is more data-driven, debugger features such as conditional breakpoints suddenly become very useful. For example, setting a breakpoint in a member function for a particular instance of a class may become trivial if your debugger supports commands of the form "Stop in ClassName::Function if this==&instance."

Handling all of C++ features in an intelligent way is a challenge for a debugger. A good debugger will allow you to think in C++, type in C++ names, and call C++ functions properly. Better yet, it will resemble a Smalltalk browser, automatically cross-referencing classes and instances.

Compiling with -g (or the equivalent) adds debugging information to an executable, but there may be other switches on your compiler that provide increased functionality. For example, some debuggers are incapable of setting breakpoints in inline functions unless inlining has been turned off at compile time. Listing Six (page 115) shows the Make file for our sample programs.

If the C++ implementation is capable of it, it may produce C source code as one of the steps in the compilation sequence. There are debuggers that allow you to step through the C code in much the same way as you would step through assembler when debugging C.

Null Pointer Dereferencing

In C++, a common problem is to call a member function using a null pointer to instance. If the pointer ptr is NULL, then ptr->function( ) behaves quite differently, depending on the type of the function. If function( ) is static, it will be called and nothing out of the ordinary will occur; the pointer value is not passed. If function( ) is non-virtual, then it will be called, but references within the function will attempt to access data at location 0. If, however, function( ) is virtual, then C++ will dereference the pointer in an attempt to access the virtual function table for that class. An (effectively) random pointer to function will be read, and the program will probably fly south for the winter.

Debuggers with hardware capabilities (such as add-in boards or access to CPU debug registers) usually allow trapping on read access. This feature is not only useful to trace program access to a particular variable, but, when location 0 is trapped, should catch attempts to access data through null pointers.

Conclusion

The key to C++ (as to most object-oriented languages) is that only a small, known section of the program deals with manipulating a given type of data. If that data is wrong, then only a known portion of the program (the member functions) can be responsible. Careful design will insure that any given member function expects an internally consistent object when called, and leaves the object correct on leaving.

_OBJECT-ORIENTED DEBUGGING_ by Simon Tooke

[LISTING ONE]

<a name="0239_0016">

#ifndef STRING_H
#define STRING_H

#include <string.h>
#include <memory.h>
#include <malloc.h>

#ifdef NULL
# undef NULL
#endif
#define NULL 0

typedef enum { False, True } Boolean;

// a String is a simple implementation of a C++ string class
class String
{
    char     *s;        // actual pointer to text
  public:
    String(void)           { s = strdup(""); }
    String(char *c)        { s = strdup(c); }
    String(char *c, int n) { s = new char[n+1]; memcpy(s,c,n); s[n]=0; }
    String(String &ss, char *c) { s = malloc(strlen(ss.s)+strlen(c)+1);
                                                 strcpy(s,ss.s); strcat(s,c); }
    String(String &ss)     { s = strdup(ss.s); }
    ~String(void)          { delete s; }
    operator char *(void)  { return s; }
    String& operator +(char *c)  { String *a = new String(*this,c); return *a;}
    String& operator +=(char *c) { *this = *this + c; return *this; }
    String& operator =(char *c)  { delete s; s = strdup(c); return *this; }
    String& operator =(String& a){ delete s; s = strdup(a.s); return *this; }
    operator ==(char *c) const;
    operator ==(String *c) const;
#ifdef DEBUG
    void dump() const;
    Boolean verify() const;
#endif /*DEBUG*/
};

// a StringListElement is a single item in a StringList
class StringListElement
{
    String s;                   // this String in the list
    StringListElement *next;    // pointer to next element in list
  public:
    StringListElement(void)     : next(NULL), s("")  {}
    StringListElement(char *c)  : next(NULL), s(c)   {}
    StringListElement(char *c, int n) : next(NULL), s(c,n) {}
    StringListElement(String &ss)     : next(NULL), s(ss)  {}
    ~StringListElement(void)    { if (next) delete next; next=NULL; }
    friend class StringList;
    friend class StringListIterator;
#ifdef DEBUG
    void dump() const;
    Boolean verify() const;
#endif /*DEBUG*/
};

// a StringList is a simple single-linked list of strings
class StringList
{
    StringListElement *head;    // first String in list
    StringListElement *tail;    // last String in list
  public:
    StringList(void) : head(NULL), tail(NULL) {}
    StringList(String& ss) { head = tail = new StringListElement(ss); }
    StringList(char *ss)   { head = tail = new StringListElement(ss); }
    ~StringList(void)      { if (head) { delete head; head=NULL; } }
    String& find(char *s) const;
    void clear(void)       { delete head; head = tail = NULL; }
    StringList& operator +=(StringList& xx);
    StringList& operator +=(char *ss);
    StringList& operator +=(String& ss);
    StringList& operator =(StringList& xx);
    operator int();
    friend class StringListIterator;
#ifdef DEBUG
    void dump() const;
    Boolean verify() const;
#endif /*DEBUG*/
};

// a StringListIterator is a method of traversing a list of strings
class StringListIterator
{
    const StringList *list;    // StringList to be traversed
    StringListElement *nxt;    // current String in StringList
  public:
    StringListIterator(void) : list(NULL), nxt(NULL) {}
    StringListIterator(const StringList *l) : list(l), nxt(l->head) {}
    String *next(void);
    void reset()        { nxt = (list != NULL) ? list->head: NULL; }
    int anymore() const { return (nxt != NULL); }
    StringListIterator& operator =(const StringList& ss);
#ifdef DEBUG
    void dump() const;
    Boolean verify() const;
#endif /*DEBUG*/
};
#endif // STRING_H




<a name="0239_0017"><a name="0239_0017">
<a name="0239_0018">
[LISTING TWO]
<a name="0239_0018">

#include <stream.h>
#include "String.h"

int main (int, char *[])
{
    String a("Hello ");
    String *b = new String("world.");
    String c;

    c = a + *b + "\n";

    cout << "a + b = " << (char *)c;
    cout << "a = " << (char *)a << "\n";
    cout << "b = " << (char *)*b << "\n";

    StringList l(a);
    l += *b;

    l.dump();
}




<a name="0239_0019"><a name="0239_0019">
<a name="0239_001a">
[LISTING THREE]
<a name="0239_001a">

#ifndef ASSERT_HDR
#define ASSERT_HDR

#ifdef DEBUG
extern void _assertRtn(char *, int);

# define assert(condition) \
    if (condition) ; else _assertRtn(__FILE__,__LINE__);

#else /*ifndef DEBUG*/

# define assert(condition)

#endif

#endif /*ASSERT_HDR*/



<a name="0239_001b"><a name="0239_001b">
<a name="0239_001c">
[LISTING FOUR]
<a name="0239_001c">

#include <stream.h>

#ifdef DEBUG
void _assertRtn(char *file, int line)
{
    cerr << "\nAssertion Failure in file '" << file
                          << "' line " << line << "\n";
    line = 0; line /= line;    // force core dump
}
#endif




<a name="0239_001d"><a name="0239_001d">
<a name="0239_001e">
[LISTING FIVE]
<a name="0239_001e">

#include "String.h"
#include "Assert.h"

#ifdef DEBUG
# include <stream.h>
#endif

/*****  String class  ******/
// String comparison operator
String::operator ==(char *c) const
{
    assert(this->verify());

    // compare String to char array
    return strcmp(s,c) == 0;
}

// String comparison operator
String::operator ==(String *c) const
{
    assert(this->verify());
    // compare String to String
    return strcmp(s,(char *)c) == 0;
}
#ifdef DEBUG
void String::dump(void) const
{
    assert(this->verify());
    cerr << "String(\"" << s << "\")";
}
Boolean String::verify(void) const
{
    // Strings must always point to something.
    if (s == NULL) return False;
    return True;
}
#endif

/****** StringList class (and StringListElement)  ******/
StringList& StringList::operator +=(String& ss)
{
    assert(this->verify());
    if (tail)
    {
        tail->next = new StringListElement(ss);
        tail = tail->next;
    }
    else
        head = tail = new StringListElement(ss);
    return *this;
}
StringList& StringList::operator +=(char *ss)
{
    assert(this->verify());
    if (tail)
    {
        tail->next = new StringListElement(ss);
        tail = tail->next;
    }
    else
        head = tail = new StringListElement(ss);
    return *this;
}
StringList& StringList::operator +=(StringList& xx)
{
    assert(this->verify());
    // add new list to old list item by item
    for (StringListElement *le=xx.head; le; le=le->next)
        *this += le->s;
    return *this;
}
// StringList assignment operator (performs deep copy)
StringList& StringList::operator =(StringList& xx)
{
    assert(this->verify());
    // get rid of old list
    clear();
    // add new list to (clear) old list item by item
    for (StringListElement *le=xx.head; le; le=le->next)
        *this += le->s;
    // return new copy of old list
    return *this;
}
// (int)(StringList) cast returns number of strings in list
StringList::operator int()
{
    int count = 0;
    StringListIterator ll(this);

    assert(this->verify());
    while (ll.next() != NULL)
        count++;
    return count;
}
#ifdef DEBUG
//
//        dump() - display instance in format "StringList(...)"
//
void StringList::dump(void) const
{
    // check consistancy
    assert(this->verify());
    // print header
    cerr << "StringList(";
    // use StringListElement::dump() to recursively display all members
    if (head != NULL) head->dump();
    // print trailer
    cerr << ")\n";
}
Boolean StringList::verify(void) const
{
    // if there are elements in this list, ensure they are valid.
    // (note that head->verify() ensures the entire list is valid.)
    if ((head!=NULL) && !head->verify()) return False;

    // Both the head and tail must either be null or non-null.
    if ((head!=NULL) && (tail==NULL)) return False;
    if ((head==NULL) && (tail!=NULL)) return False;

    return True;
}
#endif /*DEBUG*/

#ifdef DEBUG
void StringListElement::dump(void) const
{
    assert(this->verify());
    s.dump();
    if (next != NULL)
    {
        cerr << ',';
        next->dump();
    }
}
Boolean StringListElement::verify(void) const
{
    // An element of a list of Strings must point to a valid String.
    if ((char *)(s) == NULL) return False;
    if (!s.verify()) return False;
    // If there is another element within this list, it must be valid.
    if ((next!=NULL) && !next->verify()) return False;

    return True;
}
#endif

/****** StringListIterator class  ******/
// assignment operator
StringListIterator& StringListIterator::operator =(const StringList& ss)
{
    assert(this->verify());
    list = &ss;
    nxt = ss.head;
    return *this;
}
// get next item in list of strings pointed to by iterator
String *StringListIterator::next(void)
{
    assert(this->verify());
    if (list == NULL) return NULL;   // no StringList, so no next item
    if (nxt == NULL) return NULL;    // at end of list, so no next item
    String *aa = &(nxt->s);          // save pointer to String
    nxt = nxt->next;                 // point to next item in list
    return aa;                       // return pointer to String
}
#ifdef DEBUG
Boolean StringListIterator::verify(void) const
{
    // if there is a list available, verify it.
    if (!list->verify()) return False;

    // if we haven't reached the end of the list,
    // verify the next element
    if (nxt != NULL && !nxt->verify()) return False;

    // everything appears correct
    return True;
}
#endif /*DEBUG*/



<a name="0239_001f"><a name="0239_001f">
<a name="0239_0020">
[LISTING SIX]
<a name="0239_0020">

CC = CC
CFLAGS = -DDEBUG

prog : main.o String.o lib.o
    $(CC) main.o String.o lib.o -o prog

String.o : String.C String.h
    $(CC) -c $(CFLAGS) String.C

main.o : main.C String.h
    $(CC) -c $(CFLAGS) main.C

lib.o : lib.C
    $(CC) -c $(CFLAGS) lib.C

clean :
    rm -f *.o a.out core

clobber : clean
    rm -f prog












Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.