Using Macros for Verification and Reporting

A common way of keeping track of what is going on in an application during the debugging process is to use printf statements in code such as the following:

#ifdef _DEBUG
   if ( someVar > MAX_SOMEVAR )
      printf( "OVERFLOW! In NameOfThisFunc( ),
               someVar=%d, otherVar=%d.\n",
               someVar, otherVar );
#endif

The _ASSERT, _ASSERTE, _RPTn, and _RPTFn macros defined in the CRTDBG.H header file provide a variety of more concise and flexible ways to accomplish the same task. These macros automatically disappear in your release build when _DEBUG is not defined, so there is no need to enclose them in #ifdefs. For debug builds, they provide a range of reporting options that can be directed to one of several debugging destinations. The following table summarizes these options:

Macro Reporting Option
_ASSERT If an asserted expression evaluates to FALSE, the macro reports the file name and line number of the _ASSERT, under the _CRT_ASSERT report category.
_ASSERTE Same as _ASSERT, except that it also reports a string representation of the expression that was asserted to be true but was evaluated to be false.
_RPTn
(where n is 0, 1, 2, 3, or 4)
These five macros send a message string and from zero to four arguments to the report category of your choice. In the cases of macros _RPT1 through _RPT4, the message string serves as a printf-style formatting string for the arguments.
_RPTFn
(where n is 0, 1, 2, 3, or 4)
Same as _RPTn , except that these macros also include in each report the file name and line number at which the macro was executed.

Asserts are used to check specific assumptions you make in your code. _ASSERTE is a little more convenient to use because it reports the asserted expression that turned out to be false. Often this tells you enough to identify the problem without going back to your source code. A disadvantage, however, is that every expression asserted using _ASSERTE must be included in the debug version of your application as a string constant. If you use so many asserts that these string expressions take up a significant amount of memory, you may prefer to use _ASSERT instead.

Examining the definitions of these macros in the CRTDBG.H header file can give you a detailed understanding of how they work. When _DEBUG is defined, for example, the _ASSERTE macro is defined essentially as follows:

#define _ASSERTE(expr) \
   do { \
      if (!(expr) && (1 == _CrtDbgReport( \
          _CRT_ASSERT, __FILE__, __LINE__, #expr))) \
         _CrtDbgBreak(); \
    } while (0)

If expr evaluates to TRUE, execution continues uninterrupted, but if expr evaluates to FALSE, _CrtDbgReport is called to report the assertion failure. If the destination is a message window in which you choose Retry, _CrtDbgReport returns 1 and _CrtDbgBreak calls the debugger.

A single call to _ASSERTE could be used to replace the printf code at the beginning of this topic:

_ASSERTE(someVar <= MAX_SOMEVAR);

If _CRT_ASSERT reports were being directed to message boxes (the default) or to the debugger, then program execution would be interrupted when someVar exceeded MAX_SOMEVAR.

Asserts can also be used as a simple debugging error-handling mechanism for any function that returns FALSE when it fails. For example, in the following code, the assertion will fail if corruption is detected in the heap:

_ASSERTE(_CrtCheckMemory());

The following memory-checking functions can be used in asserts of this kind to verify pointers, memory ranges, and specific memory blocks:

_CrtIsValidHeapPointer

Verifies that a given pointer points to memory in the local heap; “local” here refers to the particular heap created and managed by this instance of the C run-time library. A dynamic-link library (DLL) could have its own instance of the library, and therefore its own heap, independent of your application’s local heap. Note that this routine catches not only null or out-of-bounds addresses, but also pointers to static variables, stack variables, and any other nonlocal memory.

_CrtIsValidPointer

Verifies that a given memory range is valid for reading or writing.

_CrtIsMemoryBlock

Verifies that a specified block of memory is in the “local” heap and has a valid block type. This function can actually do more than check a block’s validity, however. If you pass it non-null values for the request number, file name and/or line number, it sets the value in the block’s header accordingly.

For more information on how these and other assertion-checking routines can be used during the debugging process, see Debugging Assertions.

The printf code at the start of this topic reported actual values of someVar and otherVar to stdout. If these values were useful in the debugging process, one of the _RPTn or _RPTFn macros could be used to report them. The _RPTF2 macro, for example, is defined essentially as follows in CRTDBG.H:

#define _RPTF2(rptno, msg, arg1, arg2) \
   do { \
      if (1 == _CrtDbgReport(rptno, __FILE__, \
              __LINE__, msg, arg1, arg2)) \
         _CrtDbgBreak(); \
   } while (0)

The following call to _RPTF2 would report the values of someVar and otherVar, together with the file name and line number, every time the function that contained the macro was executed:

_RPTF2(_CRT_WARN, "In NameOfThisFunc( ), someVar= %d,
       otherVar= %d\n", someVar, otherVar);

Of course, you may only be interested in knowing the values of someVar and otherVar under the circumstance that someVar has exceeded its maximum permitted value. By using an assert, as described above, you could halt program execution and then use the debugger to examine the values of these variables. Alternatively, you could use a variation of the original printf code, enclosing a conditional call to the _RPTF2 macro in #ifdefs:

#ifdef _DEBUG
   if (someVar > MAX_SOMEVAR)
      _RPTF2(_CRT_WARN,
"In NameOfThisFunc( ), someVar= %d, otherVar= %d\n",
             someVar, otherVar );
#endif

Of course, if you find that a particular application needs a kind of debug reporting that the macros supplied with the C run-time library do not provide, you can write a macro designed specifically to fit your own requirements. In one of your header files, for example, you could include code like the following to define a macro called ALERT_IF2:

#ifndef _DEBUG                /* For RELEASE builds */
#define  ALERT_IF2(expr, msg, arg1, arg2)  ((void)0)
#else                         /* For DEBUG builds   */
#define  ALERT_IF2(expr, msg, arg1, arg2) \
   do { \
      if ((expr) && \
          (1 == _CrtDbgReport(_CRT_ERROR, \
               __FILE__, __LINE__, msg, arg1, arg2))) \
         _CrtDbgBreak( ); \
   } while (0)
#endif

One call to ALERT_IF2 could perform all the functions of the printf code at the start of this topic:

ALERT_IF2(someVar > MAX_SOMEVAR, "OVERFLOW! In NameOfThisFunc( ), someVar=%d, otherVar=%d.\n", someVar, otherVar );

This approach can be particularly useful as your debugging requirements evolve, because a custom macro can easily be changed to report more or less information to different destinations, depending on what is more convenient.