An Improved assert() for C++ Builder

By: David Pankhurst

Abstract: The assert() call allows you to test for problems in code, but it is limited. This article details a much-improved assert call.

An Improved assert() for C++ Builder

As part of good programming, assertions in your code are a convenient way to monitor what is happening. This article discusses adding flexibility to assert() to make it more useful.

Some Background on Asserting

For many years, I've used assert() to manage code:
  assert(int test)
This function takes a test and asserts the test result is true - nothing happens if so, but false results in an alert, allowing your program to detect errors conveniently. The beauty of assert() is that it is also a macro, meaning that if you #define NDEBUG, the assert macro disappears from your code, leaving nothing behind.

While this makes it useful for checking for problems in code, there are some problems with assert() itself:
  • The default for a failed assertion is to abort the program, making an assertion a rather final test.
  • assert() tests each time it is reached, making it awkward to use in a loop.
  • It's difficult to examine code; since assert() normally calls abort(), it's not possible to fail an assertion and continue debugging.
ASSERTING()

Of course, these problems are exactly those solved in this article. By modifying assert(), we get a more robust and useful way to verify code. The result is a macro package I call ASSERTING(), both to make it stand out in code, and to differentiate it from assert(). It is included in the the example code, in the files assert2.cpp/.dfm/.h:
  ASSERTING(int test,char *errorMessage)

As with the regular assert(), NDEBUG determines whether the code is actually created or not. It also adds an explanatory message to help in understanding the error.

The macro in the header file shows how it should be processed. If NDEBUG is not defined, the following code is expanded at each ASSERTING() call:


  #define ASSERTING(test,msg)                                       
  {                                                                 
    if (!(test))                                                    
    {                                                               
      static int callIt=1;                                          
      if (callIt)                                                   
      {                                                             
        if (HandleAsserting(#test,#msg,__FILE__,__LINE__,&callIt))  
        { _asm { int 3 } }                                          
      }                                                             
    }                                                               
  }

The macro uses a static variable to determine if the call is made. Our subroutine HandleAsserting() can turn off this static, allowing us to disable future testing at this location (for instance, after the first failure in a loop). Returning true from the function executes the assembly call 'int 3', which breaks to the debugger just after the assertion.

A Trio of Options

Combining these features, the HandleAsserting() call has three options:
  • Prevent further testing and 'firing' of the assertion by setting the static flag to zero.
  • Break to the debugger by returning true.
  • Continue execution by returning false.
A simple non-VCL implementation does this, and is included the assert2.cpp file at the end (but commented out):

  int HandleAsserting(char *testStr,char *msgStr,char *fileStr,int line,int *callFlag)
  {
    // assert message & and return flag regarding aborting:
    // callFlag set if repeating forbidden
    static char s_text[199]=""; // don't assign dynamically in 
                                // case of 'out of memory' error
    wsprintf(s_text,"FAILED: %srn"
                    "Error: ( %s )rn"
                    "File '%s', Line %drn"
                    "Abort execution, allow assert Retry, or Ignore in future?",
                    msgStr,testStr,fileStr,line);
    switch ( ::MessageBox(NULL,s_text,"ASSERTION ERROR",MB_ABORTRETRYIGNORE) )
    {
      case IDIGNORE: // prevent calling again - turn off flag
        *callFlag=0; // never call again
        break;
      case IDABORT:  // return flag and break
        return 1;    // abort/break
    }
    return 0;
  }
This function calls MessageBox() to display the assertion failure, and uses the Abort/Retry/Ignore buttons to get the three possibilities.

While this is perfectly usable, we have Builder at our beck and call, and of course, a VCL form can be custom tailored to our needs. The source code for this article includes a TRichEdit control, and formats the assertion error quite vibrantly. The demo program allows you to fire assertions, and try out the various options. One caveat about the VCL version - avoid using it to check items in other form's constructors.

Conclusion

Assertions are a handy way to ensure code is acting the way you expect, without adding code bloat. These options provide additional features you should find quite useful, expanding the opportunities for using assertions.

Server Response from: ETNASC02