Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
70 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Contents
 Introduction
 Assertions
 Exceptions
 Return Values
 and Logging

 One More Thing...

 Printable version
 Discuss this article
 in the forums


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:

  1. The pointer is null, or
  2. 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 <cassert>), 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:

  1. It also requires that you _think_ conciously about the assumptions your code makes on its environment and on other code/data, and thus it gives you the opportunity to develop better techniques and prevent bugs.
  2. By placing assertions near the top of the function and/or right before and after their conditions are used, other programmers will be able to more easily prevent bugs in the way they use your code.
  3. It helps to communicate the intent of your code, its affects on related functions/data, and any design limitations it might have.
  4. Bugs are easier to track down when using assertions to check key parameters, because they are found when the arguments are passed into the function, not in some obscure algorithm or function down the road, or even worse, going undetected until they cause other errors.
  5. It makes it easier to track incorrect return values (see assert( SUCCEEDED(hr) ) with DirectX) ;)
  6. You don't have to think up exception descriptions or return value error codes, and this is helpful when you are simply writing new code and wish to test something quickly, and for painfully obvious stuff like assert( this ).




Next : Exceptions