Custom C++ assert macro
Asked Answered
N

3

30

I stumbled upon an informative article: http://cnicholson.net/2009/02/stupid-c-tricks-adventures-in-assert/ which pointed out a great number of problems that exist in my current suite of debugging macros.

The full code for the final version of the macro is given near the end of the article if you follow the link.

The general form as presented is like this (somebody please correct me if i am wrong in transposing it):

#ifdef DEBUG
#define ASSERT(cond) \  
    do \  
    { \  
        if (!(cond)) \  
        { \  
            ReportFailure(#cond, __FILE__, __LINE__, 0); \
            HALT(); \
        } \  
    } while(0)  
#else  
#define ASSERT(cond) \  
    do { (void)sizeof(cond); } while(0) 

While thinking about modifying my code with what I have learned, I noticed a couple of interesting variations posted in the comments for that article:

One was that you cannot use this macro with the ternary operator (i.e. cond?ASSERT(x):func()), and the suggestion was to replace the if() with the ternary operator and some parentheses as well as the comma operator. Later on another commenter provided this:

#ifdef DEBUG
#define ASSERT(x) ((void)(!(x) && assert_handler(#x, __FILE__, __LINE__) && (HALT(), 1)))
#else
#define ASSERT(x) ((void)sizeof(x))
#endif

I thought the use of logical and && is particularly smart in this case and it seems to me that this version is more flexible than one using the if or even the ternary ?:. Even nicer is that the return value of assert_handler can be used to determine if the program should halt. Although I am not sure why it is (HALT(), 1) instead of just HALT().

Are there any particular shortcomings with the second version here that I have overlooked? It does away with the do{ } while(0) wrapped around the macros but it seems to be unnecessary here because we don't need to deal with ifs.

What do you think?

Neighbors answered 9/3, 2011 at 21:26 Comment(3)
See also my answer below: https://mcmap.net/q/474923/-custom-c-assert-macroDogcart
Note, that sizeof is not applicable to functions and elements of bit-fields. As suggested here #4031459, you can use ((void)(true ? 0 : ((x), void(), 0))); instead.Siusan
One of the features of assert is to NOT GENERATE ANY CODE on release build. On release #define ASSERT(x)Spiny
B
31

In C and C++ standard library, assert is a macro that is required to act as a function. A part of that requirement is that users must be able to use it in expressions. For example, with standard assert I can do

int sum = (assert(a > 0), a) + (assert(b < 0), b);

which is functionally the same as

assert(a > 0 && b < 0)
int sum = a + b;

Even though the former might not be a very good way to write an expression, the trick is still very useful in many more appropriate cases.

This immediately means that if one wants their own custom ASSERT macro to mimic standard assert behavior and usability, then using if or do { } while (0) in the definition of ASSERT is out of question. One is limited to expressions in that case, meaning using either ?: operator or short-circuiting logical operators.

Of course, if one doesn't care about making a standard-like custom ASSERT, then one can use anything, including if. The linked article doesn't even seem to consider this issue, which is rather strange. In my opinion, a function-like assert macro is definitely more useful than a non-function-like one.

As for the (HALT(), 1)... It is done that way because && operator requires a valid argument. The return value of HALT() might not represent a valid argument for &&. It could be void for what I know, which means that a mere HALT() simply won't compile as an argument of &&. The (HALT(), 1) always evaluates to 1 and has type int, which is always a valid argument for &&. So, (HALT(), 1) is always a valid argument for && regardless of the type of HALT().

Your last comment about do{ } while(0) does not seem to make much sense. The point of enclosing a macro into do{ } while(0) is to deal with external ifs, not the ifs inside the macro definition. You always have to deal with external ifs, since there's always a chance that your macro will be used in an external if. In the latter definition do{ } while(0) is not needed because that macro is an expression. And being an expression, it already naturally has no problems with external ifs. So, there's no need to do anything about them. Moreover, as I said above, enclosing it into do{ } while(0) would completely defeat its purpose, turning it into a non-expression.

Blunder answered 9/3, 2011 at 21:43 Comment(2)
“In my opinion, an function-like assert macro is definitely more useful than a non-function-like one.” – Why? When is this every useful? I think assertions should always stand alone in code and never be used in expressions.Grader
I would say it is superior because the assert is able to be used in an expression as well as in the typical "stand alone" way. More choices for the user. @AndreyT, thanks for making crystal clear this distinction which I kind of glazed over. It looked pretty cool at first, but now it appears that the do{}while(0) construct isn't terribly useful at all. The truth is, I had been using quite a few macros which consisted of nothing but a big if statement, which is at this point clearly not the right thing to do.Neighbors
D
12

For the sake of completeness, I published a drop-in 2 files assert macro implementation in C++:

#include <pempek_assert.h>

int main()
{
  float min = 0.0f;
  float max = 1.0f;
  float v = 2.0f;
  PEMPEK_ASSERT(v > min && v < max,
                "invalid value: %f, must be between %f and %f", v, min, max);

  return 0;
}

Will prompt you with:

Assertion 'v > min && v < max' failed (DEBUG)
  in file e.cpp, line 8
  function: int main()
  with message: invalid value: 2.000000, must be between 0.000000 and 1.000000

Press (I)gnore / Ignore (F)orever / Ignore (A)ll / (D)ebug / A(b)ort:

Where

  • (I)gnore: ignore the current assertion
  • Ignore (F)orever: remember the file and line where the assertion fired and ignore it for the remaining execution of the program
  • Ignore (A)ll: ignore all remaining assertions (all files and lines)
  • (D)ebug: break into the debugger if attached, otherwise abort() (on Windows, the system will prompt the user to attach a debugger)
  • A(b)ort: call abort() immediately

You can find out more about it there:

Hope that helps.

Dogcart answered 17/2, 2014 at 10:57 Comment(2)
Thanks, this is pretty neat. I'll stick with my own solution for now, though I will say, it looks like an ugly hack compared to what you have here.Neighbors
Feel free to ping me if you ever decide to switch. I tested it on Mac, Linux, Windows, iOS and Android so far.Dogcart
B
6

Although I am not sure why it is (HALT(), 1) instead of just HALT().

I imagine HALT may be a macro (or other stand-in name) for exit. Say we wanted to use exit(1) for our HALT command. exit returns void, which cannot be evaluated as the second argument to &&. If you use the comma operator, which evaluates its first argument, then evaluates and returns the value of it's second argument, we have an integer (1) to return to &&, even though we never reach that point because HALT() will cause us to stop long before then.

Basically, any function that fills in for HALT is probably going to have a return value of void, because it would make no sense for it to return any value. We could make it return an int, just for the sake of the macro, but if we're already hacking around with a macro a little more hackery can't hurt, can it?

Bruise answered 9/3, 2011 at 21:47 Comment(1)
So this was a way to make the compiler happy and call exit as the side effect (terminating the program! quite a side-effect i must say) by giving it a value to return. Neat.Neighbors

© 2022 - 2024 — McMap. All rights reserved.