MindView Inc.
[ Viewing Hints ] [ Revision History ] [ Book Home Page ] [ Free Newsletter ]
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ]

Thinking in C++, 2nd edition, Volume 2
Revision 4.0

by Bruce Eckel & Chuck Allison
©2001 MindView, Inc.

[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]

3: Debugging Techniques

Intro stuff

Intro stuff

Shared objects & reference counting

Reference-counted class hierarchies

Debugging

This section contains some tips and techniques which may help during debugging.

Trace macros

Sometimes it’s very helpful to print the code of each statement before it is executed, either to cout or to a trace file. Here’s a preprocessor macro to accomplish this:

#define TRACE(ARG) cout << #ARG << endl; ARG

Now you can go through and surround the statements you trace with this macro. Of course, it can introduce problems. For example, if you take the statement:

for(int i = 0; i < 100; i++)
  cout << i << endl;

And put both lines inside TRACE( ) macros, you get this:

TRACE(for(int i = 0; i < 100; i++))
TRACE(  cout << i << endl;)

Which expands to this:

cout << "for(int i = 0; i < 100; i++)" << endl;
for(int i = 0; i < 100; i++)
  cout << "cout << i << endl;" << endl;
cout << i << endl;

Which isn’t what you want. Thus, this technique must be used carefully.

A variation on the TRACE( ) macro is this:

#define D(a) cout << #a "=[" << a << "]" << nl;

If there’s an expression you want to display, you simply put it inside a call to D( ) and the expression will be printed, followed by its value (assuming there’s an overloaded operator << for the result type). For example, you can say D(a + b). Thus you can use it anytime you want to test an intermediate value to make sure things are OK.

Of course, the above two macros are actually just the two most fundamental things you do with a debugger: trace through the code execution and print values. A good debugger is an excellent productivity tool, but sometimes debuggers are not available, or it’s not convenient to use them. The above techniques always work, regardless of the situation.

Trace file

This code allows you to easily create a trace file and send all the output that would normally go to cout into the file. All you have to do is #define TRACEON and include the header file (of course, it’s fairly easy just to write the two key lines right into your file):

//: C03:Trace.h
// Creating a trace file
#ifndef TRACE_H
#define TRACE_H
#include <fstream>

#ifdef TRACEON
ofstream TRACEFILE__("TRACE.OUT");
#define cout TRACEFILE__
#endif

#endif // TRACE_H ///:~

Here’s a simple test of the above file:

//: C03:Tracetst.cpp
// Test of trace.h
#include "../require.h"
#include <iostream>
#include <fstream>
using namespace std;

#define TRACEON
#include "Trace.h"

int main() {
  ifstream f("Tracetst.cpp");
  assure(f, "Tracetst.cpp");
  cout << f.rdbuf();
} ///:~

This also uses the assure( ) function defined earlier in the book.

Abstract base class for debugging

In the Smalltalk tradition, you can create your own object-based hierarchy, and install pure virtual functions to perform debugging. Then everyone on the team must inherit from this class and redefine the debugging functions. All objects in the system will then have debugging functions available.

Finding memory leaks

  1. For array bounds checking, use the Array template in C16:Array3.cpp of Volume 1 for all arrays. You can turn off the checking and increase efficiency when you’re ready to ship. (This doesn’t deal with the case of taking a pointer to an array, though – perhaps that could be templatized somehow as well).
  2. Use the C11:MemCheck to guarantee that dynamic memory is released properly.
  3. Check for non-virtual destructors in base classes.

Tracking new/delete & malloc/free

Common problems with memory allocation include calling delete for things you have malloced, calling free for things you allocated with new, forgetting to release objects from the free store, and releasing them more than once. This section provides a system to help you track these kinds of problems down.

To use the memory checking system, you simply link the obj file in and all the calls to malloc( ), realloc( ), calloc( ), free( ), new and delete are intercepted. However, if you also include the following file (which is optional), all the calls to new will store information about the file and line where they were called. This is accomplished with a use of the placement syntax for operator new (this trick was suggested by Reg Charney of the C++ Standards Committee). The placement syntax is intended for situations where you need to place objects at a specific point in memory. However, it allows you to create an operator new with any number of arguments. This is used to advantage here to store the results of the __FILE__ and __LINE__ macros whenever new is called:

//: C03:MemCheck.h
// Memory testing system
// This file is only included if you want to
// use the special placement syntax to find
// out the line number where "new" was called.
#ifndef MEMCHECK_H
#define MEMCHECK_H
#include <cstdlib>  // size_t

// Use placement syntax to pass extra arguments.
// From an idea by Reg Charney:
void* operator new(
  std::size_t sz, char* file, int line);
#define new new(__FILE__, __LINE__)

#endif // MEMCHECK_H ///:~

In the following file containing the function definitions, you will note that everything is done with standard IO rather than iostreams. This is because, for example, the cout constructor allocates memory. Standard IO ensures against cyclical conditions that can lock up the system.

//: C03:MemCheck.cpp {O}
// Memory allocation tester
//{-msc}
#include <cstdlib>
#include <cstring>
#include <cstdio>
using namespace std;
// MemCheck.h must not be included here

// Output file object using cstdio
// (cout constructor calls malloc())
class OFile {
  FILE* f;
public:
  OFile(char* name) : f(fopen(name, "w")) {}
  ~OFile() { fclose(f); }
  operator FILE*() { return f; }
};
extern OFile memtrace;
// Comment out the following to send all the
// information to the trace file:
#define memtrace stdout

const unsigned long _pool_sz = 50000L;
static unsigned char _memory_pool[_pool_sz];
static unsigned char* _pool_ptr = _memory_pool;

void* getmem(size_t sz) {
  if(_memory_pool + _pool_sz - _pool_ptr < sz) {
    fprintf(stderr,
           "Out of memory. Use bigger model\n");
    exit(1);
  }
  void* p = _pool_ptr;
  _pool_ptr += sz;
  return p;
}

// Holds information about allocated pointers:
class MemBag { 
public:
  enum type { Malloc, New };
private:
  char* typestr(type t) {
    switch(t) {
      case Malloc: return "malloc";
      case New: return "new";
      default: return "?unknown?";
    }
  }
  struct M {
    void* mp;  // Memory pointer
    type t;     // Allocation type
    char* file; // File name where allocated
    int line;  // Line number where allocated
    M(void* v, type tt, char* f, int l)
      : mp(v), t(tt), file(f), line(l) {}
  }* v;
  int sz, next;
  enum { increment = 50 };
public:
  MemBag() : v(0), sz(0), next(0) {}
  void* add(void* p, type tt = Malloc,
            char* s = "library", int l = 0) {
    if(next >= sz) {
      sz += increment;
      // This memory is never freed, so it
      // doesn't "get involved" in the test:
      const int memsize = sz * sizeof(M);
      // Equivalent of realloc, no registration:
      void* p = getmem(memsize);
      if(v) memmove(p, v, memsize);
      v = (M*)p;
      memset(&v[next], 0,
             increment * sizeof(M));
    }
    v[next++] = M(p, tt, s, l);
    return p;
  }
  // Print information about allocation:
  void allocation(int i) {
    fprintf(memtrace, "pointer %p"
      " allocated with %s",
      v[i].mp, typestr(v[i].t));
    if(v[i].t == New)
      fprintf(memtrace, " at %s: %d",
        v[i].file, v[i].line);
    fprintf(memtrace, "\n");
  }
  void validate(void* p, type T = Malloc) {
    for(int i = 0; i < next; i++)
      if(v[i].mp == p) {
        if(v[i].t != T) {
          allocation(i);
          fprintf(memtrace,
          "\t was released as if it were "
          "allocated with %s \n", typestr(T));
        }
        v[i].mp = 0;  // Erase it
        return;
      }
    fprintf(memtrace,
    "pointer not in memory list: %p\n", p);
  }
  ~MemBag() {
    for(int i = 0; i < next; i++)
      if(v[i].mp != 0) {
        fprintf(memtrace,
        "pointer not released: ");
        allocation(i);
      }
  }
};
extern MemBag MEMBAG_;

void* malloc(size_t sz) {
  void* p = getmem(sz);
  return MEMBAG_.add(p, MemBag::Malloc);
}

void* calloc(size_t num_elems, size_t elem_sz) {
  void* p = getmem(num_elems * elem_sz);
  memset(p, 0, num_elems * elem_sz);
  return MEMBAG_.add(p, MemBag::Malloc);
}  

void* realloc(void* block, size_t sz) {
  void* p = getmem(sz);
  if(block) memmove(p, block, sz);
  return MEMBAG_.add(p, MemBag::Malloc);
}

void free(void* v) { 
  MEMBAG_.validate(v, MemBag::Malloc);
}

void* operator new(size_t sz) {
  void* p = getmem(sz);
  return MEMBAG_.add(p, MemBag::New);
}

void*
operator new(size_t sz, char* file, int line) {
  void* p = getmem(sz);
  return MEMBAG_.add(p, MemBag::New, file, line);
}

void operator delete(void* v) {
  MEMBAG_.validate(v, MemBag::New);
}

MemBag MEMBAG_;
// Placed here so the constructor is called
// AFTER that of MEMBAG_ :
#ifdef memtrace
#undef memtrace
#endif
OFile memtrace("memtrace.out");
// Causes 1 "pointer not in memory list" message
///:~

OFile is a simple wrapper around a FILE*; the constructor opens the file and the destructor closes it. The operator FILE*( ) allows you to simply use the OFile object anyplace you would ordinarily use a FILE* (in the fprintf( ) statements in this example). The #define that follows simply sends everything to standard output, but if you need to put it in a trace file you simply comment out that line.

Memory is allocated from an array called _memory_pool. The _pool_ptr is moved forward every time storage is allocated. For simplicity, the storage is never reclaimed, and realloc( ) doesn’t try to resize the storage in the same place.

All the storage allocation functions call getmem( ) which ensures there is enough space left and moves the _pool_ptr to allocate your storage. Then they store the pointer in a special container of class MemBag called MEMBAG_, along with pertinent information (notice the two versions of operator new; one which just stores the pointer and the other which stores the file and line number). The MemBag class is the heart of the system.

You will see many similarities to xbag in MemBag. A distinct difference is realloc( ) is replaced by a call to getmem( ) and memmove( ), so that storage allocated for the MemBag is not registered. In addition, the type enum allows you to store the way the memory was allocated; the typestr( ) function takes a type and produces a string for use with printing.

The nested struct M holds the pointer, the type, a pointer to the file name (which is assumed to be statically allocated) and the line where the allocation occurred. v is a pointer to an array of M objects – this is the array which is dynamically sized.

The allocation( ) function prints out a different message depending on whether the storage was allocated with new (where it has line and file information) or malloc( ) (where it doesn’t). This function is used inside validate( ), which is called by free( ) and delete( ) to ensure everything is OK, and in the destructor, to ensure the pointer was cleaned up (note that in validate( ) the pointer value v[i].mp is set to zero, to indicate it has been cleaned up).

The following is a simple test using the memcheck facility. The MemCheck.obj file must be linked in for it to work:

//: C03:MemTest.cpp
//{L} MemCheck
//{-msc}
// Test of MemCheck system
#include "MemCheck.h"

int main() {
  void* v = std::malloc(100);
  delete v;
  int* x = new int;
  std::free(x);
  new double;
} ///:~

The trace file created in MemCheck.cpp causes the generation of one "pointer not in memory list" message, apparently from the creation of the file pointer on the heap. [[ This may not still be true – test it ]]

The canonical object & singly-rooted hierarchies

An extended canonical form

Including a set of methods (such as trace/print/dump) in your library’s base class to enable easy debugging.

Exercises

  1. Create a heap compactor for all dynamic memory in a particular program. This will require that you control how objects are dynamically created and used (do you overload operator new or does that approach work?). The typical heap-compaction scheme requires that all pointers are doubly-indirected (that is, pointers to pointers) so the “middle tier” pointer can be manipulated during compaction. Consider overloading operator-> to accomplish this, since that operator has special behavior which will probably benefit your heap-compaction scheme. Write a program to test your heap-compaction scheme.
[ Previous Chapter ] [ Short TOC ] [ Table of Contents ] [ Index ] [ Next Chapter ]
Last Update:09/26/2001