다음을 통해 공유


Compound Statement Macros

I had thought this was a rather well-known trick, but, after reacquanting myself with Alexandrescu's Modern C++ Design, I've come to believe that it's not at as well-known as I thought.  So, I thought I'd share it here for posterity.

It's common to write a macro that looks like a function call, yet expands to multiple statements inline.  Classic examples, and one Alexandrescu uses, involve variations on the Assert macro.  You need to use a macro, because you want to make use of the pre-defined __LINE__ and __FILE__ macros.

The standard assert macro ends up being rather wimpy. It tells you that an assumption in your code failed for some reason, but it gives you no information on why that assumption was false at that particular point of execution. What most programmers end up doing is defining a debug-only function that gets called when an assertion fails, and sophisticated programmers write this function in such a way as to make use of some version of printf to convey information about the assertion failure.  One can also, through the magic of C/C++ preprocessors, include the text of the expression that failed to evaluate to true in the message that's output with the assertion failure.

As a result, a common way of writing the Assert macro is:

#define Assert(_expr) \
if (!(_expr)) \
{ \
const char szAssert[] = #_expr; \
AssertProc("Assertion failure: %s in %s(%d)", szAssert, __FILE__, __LINE__); \
}

The problem with that construct occurs when you put a semicolon after a use of the Assert macro.  It expands to:

   if (!(_expr))
{
const char szAssert[] = #_expr;
AssertProc("Assertion failure: %s in %s(%d)", szAssert, __FILE__, __LINE__);
};

Note the semicolon after the closing brace.  Some compilers will let you get away with this syntactical error, but quite a few will at least warn about it.  If you have warnings turned on as errors, then your code won't compile.

The solution is to wrap compund statements like this within a do{...}while(0) construct:

#define Assert(_expr) \
   do { \
if (!(_expr)) \
{ \
const char szAssert[] = #_expr; \
AssertProc("Assertion failure: %s in %s(%d)", szAssert, __FILE__, __LINE__); \
} \
while(0)

The while(0) results in a "loop" that will only ever get executed once, but the syntax of the construct resolves to the equivalent of a single statement.  You can even use it in shipping code (presumably for something other than Assert), and the compiler will optimze out the test for "0" at the end of the loop.

As something of a side note, there's likely a pedant somewhere who is looking at my Assert macro, and wondering why I added the "const char szAssert" bit.  Well, the first parameter to AssertProc is a hint.  Rather than simply using Assert(_expr), how about defining a macro "AssertSz(_expr, _sz)"?  Or, better yet, some preprocessors will allow you to use variable arguments:

#define AssertSz(_expr, _szFmt, ...) \
   do { \
if (!(_expr)) \
{ \
const char szAssert[] = _szFmt; \
AssertProc(_szFmt, __FILE__, __LINE__, __VA_ARGS__); \
} \
while(0)

The "const char szAssert" ensures that you won't inadvertently use some variable of type "char *" when you use the AssertSz macro in your code.  This could have rather nasty side-effects, including bogus assertion failures.

There is a wide variety of fun tricks you can employ with this technique, including declaring a static flag within the "if" block, the address of which gets passed into your AssertProc.  The assert handler can then display a dialog rather than simply quit, and provide an option to disable that particular assertion failure for the remainder of the program's execution.  All of this is made possible by the "do{...}while(0)" construct that makes a compund statement syntactically equivalent to a single statement.  Further usage of this idea is left as an exercise for the reader.

 

Rick

Currently Playing in iTunes: Smooth Operator by Sade

Comments

  • Anonymous
    August 31, 2005
    If you're using GCC, one additional trick for assertion macros is to use GCC's __builtin_expect in the "if (!(_expr))" part. Then the compiler will generate the branch with a default prediction of branch-not-taken, at least on architectures where you can set default branch prediction values in assembly code.

    Of course, this assumes that your assertions typically aren't hit. If they're hit often, you have other problems....
  • Anonymous
    August 31, 2005
    What happened to the feeds? (Or rather, why are they truncated - is there any hope of getting full feeds back?)
  • Anonymous
    September 01, 2005
    And this is why macros are so horrible! Too many hidden problems. I need more clarity than wrapping things in a do { ... } while (0) thanks!
  • Anonymous
    September 05, 2005
    There is also another reason for the use of the "do{...}while(0)" statement. It also prevents any problems with what I think is called a "dangling else". For example, with the "do{...}while(0)"

    if (X)
    assert(Y)
    else
    ...

    will be equivalent to
    if (X)
    {
    assert(Y)
    }
    else
    {
    ...
    }

    Without it you would get

    if (X)
    {
    // Expanded assertion
    if (!Y)
    {
    ____
    }
    else
    {
    ...
    }
    }

    Which is probably not what is intended.