Topic : C++ Debuging
Author : Null Pointer
Page : 1 Next >>
Go to page :


C++ Debugging
by null_pointer


C++ has several powerful features available for debugging no matter which platform you use, whether or not you have access to a debugger. The purpose of this article is to enumerate the methods you can use to debug your code, and discuss circumstances for their use.

When finding out about a new feature in a programming language, one's first inclination is often to ignore its drawbacks and try to substitute it for all other features. Since no design model is perfect for every problem, this inclination is wasteful and merely leads to poorly designed code, since everything must be made to fit into the "better" design model.

You cannot choose the most suitable model without first considering your circumstances and the relative strengths and weaknesses of several different methods. Assertions, exceptions, logging, return values, etc. all have specific strengths and weaknesses.

I'll list some of my observations on these methods.

Method 0 - Nothing
Pros: Easy to write tons of code, imposes no execution burden in debug or release builds

Cons: Better skip the country if it doesn't work

This method is more of a non-method - that's why it is called method 0. I thought I'd include it for completeness. If you use this method often, do your clients a favor and seek professional help.

It also gives me a chance to explain the theory of debugging as I see it and some conventions we'll use throughout this article. There are basically two versions of code in C++ - debug and release. The code must be functionally equivalent in both modes. The difference is that in debug mode we favor useful debugging aids over speed, and in release mode we often value speed over debugging. Of course, you can define different levels if need be, but for this article we'll only use two.

Note that debugging is different than cleanup.

Bugs are typically poorly designed code that fails under certain conditions. Debugging is the process of finding and eliminating bugs. There are many causes of bugs, but here is a short list:

Poor understanding of the language, API, other code, and/or platform
Bad code design/organization
Operating System bugs
Miscommunication between members of a team
Hastily written code
Absolutely no reason at all :)
Note that when you write code, your code is rarely completely independent. Viz., your code is typically dependent on the standard library. Further, you will rarely code alone (if you intend to be a commercial success), which means interdependencies will exist between your code and that of your team members, and possibly that of your customers (if you code libraries).

Two terms are often used to describe the roles of people or source code as they relate to depencies: server and client. People who use and depend on your code are said to be clients. When you depend on their code, you are their client. Server is rarely used here, but it means "the person who wrote the code."

Method 1 - Assertions
Pros: Relatively fast, imposes no overhead in a release build, extremely simple to code.

Cons: Slows the code down a little bit in a debug build, provide no safety in a release build, requires clients to read source code when they debug.

Explanation
An assertion is a boolean expression that must hold true in order for the program to continue to execute properly. You state an assertion in C++ by using the assert function, and passing it the expression that must be true:

assert( this );

If this is zero, then the assert function stops program execution, displays a message informing you that "assert( this )" failed on such and such a line in your source file, and lets you go from there. If this wasn't zero, assert will simply return and your program will continue to execute normally.

Note that the assert function does nothing and imposes no overhead in a release build, so do NOT use it like this:

FILE* p = 0;
assert( p = fopen("myfile.txt", "r+") );


...because fopen will not be called in the release version! This is the correct way to do it:

FILE* p = fopen("myfile.txt", "r+") );
assert( p );


Examples
These are best used in writing new code, where assumptions must almost always be made. Consider the following function:

void sort(int* const myarray) // an overly simple example
{
  for( unsigned int x = 0; x < sizeof(myarray)-1; x++ )
    if( myarray[x] < myarray[y] ) swap(myarray[x], myarray[y]);
}


Count the number of assumptions this function makes. Now take a look at the better version, which makes debugging a bit easier:

void sort_array(int* const myarray)
{
  assert( myarray );
  assert( sizeof(myarray) > sizeof(int*) );

  for( unsigned int x = 0; x < sizeof(myarray)-1; x++ )
    if( myarray[x] < myarray[y] ) swap(myarray[x], myarray[y]);
}


You see, that innocent-looking algorithm won't work if:

The pointer is null, or sizeof(myarray) cannot be used to determine the number of elements in the array, either because the array was not allocated on the stack, or because someone has passed in the address of a single (non-array) object.
Although that is a simple algorithm, many functions you will write and/or encounter will be much larger and more complex than that. It is surprising when you see the amount of conditions which can cause a piece of code to fail. Let's take a look at a portion of an alpha-blending routine that I was playing with a while back:
void blend(const video::memory& source,
           video::memory& destination,
           const float colors[3])
{
  // The algorithm used is: B = A * alpha

  const unsigned int width = source.width();
  const unsigned int height = source.height();
  const unsigned int depth = source.depth();
  const unsigned int pitch = source.pitch();

  switch( depth )
  {
  case 15:
    // ...
    break;

  case 16:
    // ...
    break;

  case 24:
  {
    unsigned int offset = 0;
    unsigned int index = 0;

    for( unsigned int y = 0; y < height; y++ )
    {
      offset = y * pitch;

      for( unsigned int x = destination.get_width(); x > 0; x-- )
      {
        index = (x * 3) + offset;

        destination[index + 0] = source[index + 0] * colors[0];
        destination[index + 1] = source[index + 1] * colors[1];
        destination[index + 2] = source[index + 2] * colors[2];
      }
    }
  } break;

  case 32:
    // ...
    break;
  }
}


Do you realize the amount of assumptions that function makes in the name of optimization? Let's try listing them:

assert( source.locked() and destination.locked() );
assert( source.width() == destination.width() );
assert( source.height() == destination.height() );
assert( source.depth() == destination.depth() );
assert( source.pitch() == destination.pitch() );
assert( source.depth() == 15 or source.depth() == 16
    or source.depth() == 24 or source.depth() == 32 );


Typically, the more you optimize in low-level code such as that, the more assumptions you make. My function requires that the source and destination video memory be locked (so multiple blend functions can be called with a single lock()/unlock()), and that they both have the same width, height, depth, and pitch. Placing these assertions at the top of the function will prevent some programmer in the future from wondering why my function doesn't work or causes access violations - all requirements are now stated clearly at the top of the function.

However, you have to be careful which version of assert you use. If you use the ANSI C assert (defined in ), then you may wind up with a very large debug build because of all the string constants it creates. If you find this to be a problem, override assert to trigger an exception or something else instead of building the string constants.

Also, you don't have to check every parameter and condition - some things are painfully obvious to a good programmer. Sometimes comments might be better because they do not increase the size of the final build.

Good code should not require a plethora of assertions at the top of each function. If you find that you are writing a class and you have to place assertions in each member function to test the state, etc. then it is probably better to split the class up into other classes.

Conclusion
Getting into the habit of sprinkling assertions throughout your code has the following benefits:

It also requires that you _think_ conciously about the

Page : 1 Next >>