2

What's the way to implement a standard-compliant assert macro with an optional formatted message?

What I have works in clang, but (correctly) triggers the -Wgnu-zero-variadic-macro-arguments warning if it is turned on (e.g. via -Wpedantic) when the macro is used without the optional message. Wandbox

#define MyAssert(expression, ...)                                      \
    do {                                                               \
        if(!(expression))                                              \
        {                                                              \
            printf("Assertion error: " #expression " | " __VA_ARGS__); \
            abort();                                                   \
        }                                                              \
    } while(0)
Danra
  • 9,546
  • 5
  • 59
  • 117

3 Answers3

4

One needs to really use the preprocessor to the max in order to differentiate no additional arguments from the case where they are present. But with Boost.PP one can do this:

#include <boost/preprocessor/variadic/size.hpp>
#include <boost/preprocessor/arithmetic/sub.hpp>
#include <boost/preprocessor/logical/bool.hpp>
#include <boost/preprocessor/cat.hpp>


#define MyAssert(...) BOOST_PP_CAT(MY_ASSERT,BOOST_PP_BOOL(BOOST_PP_SUB(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1)))(__VA_ARGS__)

#define MY_ASSERT0(expr) MY_ASSERT1(expr,)

#define MY_ASSERT1(expression, ...)                                    \
    do {                                                               \
        if(!(expression))                                              \
        {                                                              \
            std::printf("Assertion error: " #expression " | " __VA_ARGS__); \
            std::abort();                                              \
        }                                                              \
    } while(0)

MyAssert must accept at least one argument (standard). Then we count the arguments, subtract one, and turn to a boolean (0 or 1). This 0 or 1 is concatenated to the token MY_ASSERT to form a macro name, to which we proceed to forward the arguments.

MY_ASSERT1 (with args), is your original macro. MY_ASSERT0 substitutes itself with MY_ASSERT1(expr,), the trailing comma means we pass another argument (thus fulfilling the requirement for the one extra argument), but it is an empty token sequence, so it does nothing.

You can see it live.


Since we already went down this rabbit hole, if one doesn't want to pull in Boost.PP the above can be accomplished with the usual argument counting trick, slightly adapted. First, we must decide on a maximum limit for the arguments we allow. I chose 20, you can choose more. We'll need the typical CONCAT macro, and this macro here:

#define HAS_ARGS(...) HAS_ARGS_(__VA_ARGS__,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,)
#define HAS_ARGS_(a1,a2,a3,a4,a5,b1,b2,b3,b4,b5,c1,c2,c3,c4,c5,d1,d2,d3,d4,d5,e, N, ...) N

It's argument counting, but with a twist. When __VA_ARGS__ is a single argument (no extra ones), the N resolved as 0. Otherwise, it is resolved as 1. There can be up to 20 extra arguments after the expression, any number of which will resolve to the same 1. Now we just plug it into the same place we used boost before:

#define MyAssert(...) CONCAT(MY_ASSERT, HAS_ARGS(__VA_ARGS__))(__VA_ARGS__)

You can tinker with it here

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • While this solution is more complex than @yairchu 's, it has the advantage of allowing to annotate `assertionMessage` with `__attribute__((__format__ (__printf__, 2, 3)))` to get helpful warnings about mistakes about invalid string formats and parameters combinations passed to the assertion macro. With @yairchu's answer, adding the attribute results in `-Wformat-extra-args` warnings due to the extra `""` passed into `assertionMessage`. – Danra Jan 02 '19 at 14:36
  • This seems to work incorrectly in Visual Studio. The passed `__VA_ARGS__` to `MY_ASSERT1` are passed as a single argument! – yairchu Mar 24 '19 at 11:26
  • @yairchu - `__VA_ARGS__` being a single amaglamtion of the remaining tokens (with commas) is standard. Not discounting the possibility of a MSVC bug, but your comment isn't too informative. – StoryTeller - Unslander Monica Mar 24 '19 at 11:30
  • @StoryTeller I meant that all of `__VA_ARGS__` with a parentheses around it becomes `expression` in `MY_ASSERT1`, where the intention was only for the single argument to be `expression` and the rest to be `MY_ASSERT1`'s `__VA_ARGS__`. – yairchu Apr 14 '19 at 11:43
  • 1
    @yairchu - Well then, *definitely* a MSVC bug. It may or may not be possible to solve it with another layer of indirection. ¯\\_(ツ)_/¯ – StoryTeller - Unslander Monica Apr 14 '19 at 11:46
1

The basic solution is to use << on cerr:

#define MyAssert(expression, msg)                                  \
do {                                                               \
    if(!(expression))                                              \
    {                                                              \
        std::cerr << msg;                                          \
        abort();                                                   \
    }                                                              \
} while(0)

This solution uses C++ streams, so you can format the output as you see fit. Actually this is a simplification of a C++17 solution that I'm using to avoid temporaries (people tend to use + instead of << with this solution, triggering some efficiency warnings).

Use it then like this:

MyAssert(true, "message " << variable << " units");

I think the optionality is bogus here, as you are outputting "Assertion error:" meaning that you expect a message.

Matthieu Brucher
  • 21,634
  • 7
  • 38
  • 62
  • 1
    Thanks! The optionality is not bogus, as sometimes there is no extra useful context to add to the failed expression. Note the failed expression is *always* printed, following the colon. You might say that the optionality is bogus because I'm also printing `|` - but that may be replaced with a space, or, if one wishes, make a more complicated macro which only prints it if `__VA_ARGS__` is not empty (though it should never be empty according to the standard :)) – Danra Dec 31 '18 at 11:24
  • Oh yes, my bad... In that case, you can use @StoryTeller's trick to pass `#expression` as the second argument to `MyAssert`. – Matthieu Brucher Dec 31 '18 at 11:34
1

I have a solution which I'm not particularly proud of..

We can obtain the first argument in plain form and as a string using:

#define VA_ARGS_HEAD(N, ...) N
#define VA_ARGS_HEAD_STR(N, ...) #N

Note that in usage, in order to not get warnings, you should do VA_ARGS_HEAD(__VA_ARGS__, ) (with the extra ,) so that VA_ARGS_HEAD is never used with a single parameter (trick taken from StoryTeller's answer).

We define the following helper function:

#include <stdarg.h>
#include <stdio.h>

inline int assertionMessage(bool, const char *fmt, ...)
{
    int r;
    va_list ap;
    va_start(ap, fmt);
    r = vprintf(fmt, ap);
    va_end(ap);
    return r;
}

When the assertion has a format string, the function would work with __VA_ARGS__ as is, however when the bool is the only argument, we're missing a format string. That's why we'll add another empty string after __VA_ARGS__ when invoking it:

#define MyAssert(...)                                                          \
    do {                                                                       \
        if(!(VA_ARGS_HEAD(__VA_ARGS__, )))                                     \
        {                                                                      \
            printf("Assertion error: %s | ", VA_ARGS_HEAD_STR(__VA_ARGS__, )); \
            assertionMessage(__VA_ARGS__, "");                                 \
            abort();                                                           \
        }                                                                      \
    } while(0)

Note that assertionMessage doesn't have printf in its name. This is deliberate and intended to avoid the compiler giving format-string related warnings for its invocations with the extra "" argument. The down-side for this is that we don't get the format-string related warnings when they are helpful.

yairchu
  • 23,680
  • 7
  • 69
  • 109