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


One More Thing...

I had intended to finish this article here, but I wish to show how valuable a mixed approach to debugging can be. The easiest way to do this is by creating a simple class, preferrably one that must work with non-C++ code. A file class will do nicely. We'll use C's fopen() and related functions for simplicity and portability.

We need to meet the following requirements:

  1. Constructor and destructor that match the lifetime of the file pointer.
  2. Assertions to describe assumptions made by each function.
  3. Exception types used to force the client to handle exceptional conditions.
  4. Member functions for reading and writing data.
  5. Templated member functions as shortcuts for stack-based data.
  6. Exception safety, including a fail-safe destructor and responsible member functions.
  7. Portability.

Here it is:

#include <cstdio>
#include <cassert>
#include <ciso646>
#include <string>

class file
{
public:

  // Exceptions
  struct exception {};
  struct not_found : public exception {};
  struct end : public exception {};

  // Constants
  enum modes { relative, absolute };

  file(const std::string& filename, const std::string& parameters);
  ~file();

  void seek(const unsigned int position, const enum modes = relative);
  void read(void* const data, const unsigned int size);
  void write(const void* const data, const unsigned int size);
  void flush();

  // Stack only!
  template <typename T> void read(T& data) { read(&data, sizeof(data)); }
  template <typename T> void write(const T& data) { write(&data, sizeof(data)); }

private:
  FILE* pointer;

  file(const file& other) {}
  file& operator = (const file& other) { return( *this ); }
};


file::file(const std::string& filename, const std::string& parameters)
  : pointer(0)
{
  assert( not filename.empty() );
  assert( not parameters.empty() );

  pointer = fopen(filename.c_str(), parameters.c_str());

  if( not pointer ) throw not_found();
}


file::~file()
{
  int n = fclose(pointer);
  assert( not n );
}


void file::seek(const unsigned int position, const enum file::modes mode)
{
  int n = fseek(pointer, position, (mode == relative) ? SEEK_CUR : SEEK_SET);
  assert( not n );
}


void file::read(void* const data, const unsigned int size)
{
  size_t s = fread(data, size, 1, pointer);

  if( s != 1 and feof(pointer) ) throw end();
  assert( s == 1 );
}


void file::write(const void* const data, const unsigned int size)
{
  size_t s = fwrite(data, size, 1, pointer);
  assert( s == 1 );
}


void file::flush()
{
  int n = fflush(pointer);
  assert( not n );
}


int main(int argc, char* argv[])
{
  file myfile("myfile.txt", "w+");

  int x = 5, y = 10, z = 20;
  float f = 1.5f, g = 29.4f, h = 0.0129f;
  char c = 'I';

  myfile.write(x);
  myfile.write(y);
  myfile.write(z);
  myfile.write(f);
  myfile.write(g);
  myfile.write(h);
  myfile.write(c);

  return 0;
}

If you compile this under Windows, make sure the project type is set to "Win32 Console App." What benefits does this class provide to its clients?

  1. It allows the user to manually debug in order to see which function and thus which parameters caused the member function to fail.
  2. It checks all return values and most parameters in a debug build.
  3. It throws two types of exceptions if the condition is exceptional and the client must decide what to do (i.e., run an installation repair wizard and restore some data files from the CD, prompt the user, or whatever).
  4. It provides shortcuts for the most common operations.
  5. It disallows copy construction and copy assignment, which it is not prepared to handle.

Note that the exceptions are thrown at two points which are vital to the file pointer's state:

  1. When the file is created, we make sure it succeeded.
  2. When the file is read from, we make sure the end is not reached prematurely.

Overhead

Note that all variables used to hold the return values in preparation for evaluation by assertions should be taken out by any optimizing compiler in a release build. The assertions are not evaluated in release build. This ensures that the program runs with very little overhead. If you look at the source code for the member functions, they evaluate to basically the function call in a release build.

The exceptions are, however, present in a release build, and this is especially good because they may affect the program's ability to function and recover properly.

We don't need to use assert( this ) or assert( pointer ) in the member functions because file objects can only ever be created in a valid state. If operator new doesn't allocate a file object correctly, the constructor/destructor is never called and operator new throws an exception. If fopen doesn't return a valid file pointer, we throw a file::not_found exception and the file destructor is never called, nor are any of the member functions used. So we never have to worry about having an invalid this pointer or invalid file pointer in our member functions.

(If the user tries to call a member function using a null pointer, this will probably be null and accessing the member functions will probably cause access violations. Programmers should have experience with that though.)

If we were to put the member functions into the header files and use the inline keyword, the compiler should be able to inline those functions in the release build, eliminating the function call overhead and associated temporaries, and making the class almost as fast in a release build as if we had simply used straight C code. :)

Judicious use of assertions can make your code easier to debug without decreasing the speed or size of the final (release) build.

Conclusion

The techniques illustrated with the file class can be used for most legacy code that exposes only handles or pointers to its internal objects.

Extension of the file class's functionality is an educational exercise left to the reader. Try adding a copy constructor and assignment operator, and test the class under different conditions. Note which assertions become invalid when you modify the class, and which existing assertions help you to catch errors with new code. With your changes, is it possible to put the file object in an invalid state?

Conclusion

It is best to use assertions when debugging conditions that will almost certainly cause the code to fail anyway down the function, to use exceptions as panic buttons for release code, and return values for what they were intended - passing valid data back to the calling function. Use logs for data that you can't or don't want to check when the program is running.

Fortunately, all of these suggestions are good ol' portable C++. Unfortunately, not everyone writes in C++ :) and there are also times when clients should not need to browse your source code. You must choose which methods to use based on your circumstances, and more importantly, those of your client(s).

The sad part is that return values are often the only communication of errors between C++ code and non-C++ code. It is quite a pain to check every return value (and you should). Assertions serve to alleviate some of that pain.

- null_pointer (null_pointer_us@yahoo.com)