5

I would like to create a custom version of the assert macro defined in <cassert>, that displays an error message when the assertion fails.


Desired usage:

custom_assert(AClass<T1, T2>::aBoolMethod(), "aBoolMethod must be true");


Flawed test implementations:

#define custom_assert(mCondition, mMessage) ...
// This fails because mCondition may have commas in it

#define custom_assert(..., mMessage)
// Not sure about this either - mMessage may be an expression containing commas
// as well

How can I correctly implement a custom assert that takes a boolean expression (with possible commas) as the first argument and a string expression (with possible commas) as the second argument?

Or is there a way to implement assertions without the use of macros?

Ali
  • 56,466
  • 29
  • 168
  • 265
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • You have to have a way to distinguish between the message and the boolean values. What is it? – elyashiv Feb 22 '14 at 17:11
  • try [this](http://stackoverflow.com/questions/3767869/adding-message-to-assert) – Nikos Athanasiou Feb 22 '14 at 17:14
  • @NikosAthanasiou, correct me if I'm wrong, but won't passing a boolean expression with commas as the first argument of the macro described in the answer to the link you posted be interpreted as multiple arguments? – Vittorio Romeo Feb 22 '14 at 17:17
  • 1
    The C preprocessor has no knowledge of C++ templates. In this particular case, you'd need to add an extra set of parentheses around the first argument to get it to parse correctly, e.g. `custom_assert((AClass::aBoolMethod()), "aBoolMethod must be true");`. – Adam Rosenfield Feb 22 '14 at 17:19
  • 6
    Why does it have to be a macro? Why don't you make it a function? – dyp Feb 22 '14 at 17:20
  • @NikosAthanasiou Please write in English so everyone can understand. – John Kugelman Feb 22 '14 at 17:24
  • @JohnKugelman I said "I don't know whether it's a good idea but I'll try answering in a few" – Nikos Athanasiou Feb 22 '14 at 17:48
  • 1
    One way I've seen is `assert(expr && "some text");`, and a `custom_assert()` that supports that, with commas in expr, is trivially implementable as `#define custom_assert(...) assert((__VA_ARGS__))`. Would that be sufficient for your purposes? If you do need to keep your string separate from the expression being checked, it would be much easier to make that string the first argument, and use `#define custom_assert(msg, ...) <...>` (where again, `__VA_ARGS__` is the expression, which may contain commas) –  Feb 22 '14 at 18:18
  • If you need a macro e.g. to get the correct line number `__LINE__`, you can still use a function wrapped in a simple macro: `#define myAssert(...) myAssertFunc(__VA_ARGS__, __LINE__)` Here, both the message and the condition can contain commas. – dyp Feb 22 '14 at 23:28

4 Answers4

4

You were quite close, what you need to use is simply this:

#define myAssert(message, ...) do { \
    if(!(__VA_ARGS__)) { \
        /*error code*/ \
    } \
} while(0)

The special preprocessor variable __VA_ARGS__ will expand to whatever was passed in the place of the three dots, including all comas.

Note that the preprocessor will not interprete commas in the condition in the very least, it will just paste them as is into the if() statement. Which is precisely what you want if you want to pass templated conditions, as hinted by the comments.

Commas in the message string are not a problem either, since the preprocessor understands about string literals and does not interprete anything within the double quotes.

cmaster - reinstate monica
  • 38,891
  • 9
  • 62
  • 106
  • Ah, I completely misunderstood your objective, sorry. I have updated the answer accordingly. – cmaster - reinstate monica Feb 22 '14 at 19:06
  • 1
    +1, this is essentially what I suggested in the comments on the question too, but you've worked it out to a full answer. One thing to note, though, is that the OP is worried about `message` containing commas too. I don't see how that would be a problem (the preprocessor will see all string literals, even those containing commas, as a single token), but if you do, it would be helpful to address that in your answer. –  Feb 22 '14 at 19:10
  • 1
    The message could also be a templated expression using commas - I guess the only solution is wrapping the expressions in parenthesis – Vittorio Romeo Feb 22 '14 at 20:20
1

The straightforward

assert(AClass<T1, T2>::aBoolMethod() && "aBoolMethod must be true");

fails:

error: macro "assert" passed 2 arguments, but takes just 1

but if you add an extra pair of parenthesis around the first argument, it works. Like this:

#include <cassert>

template <typename A, typename B>
struct C { 
  bool f() { return false; }
};

int main() {
  assert((C<int,int>().f()) && "some message, with comma");
  //     ^                ^
}

Note: it was also pointed out by Adam Rosenfield in a comment.

Perhaps the only benefit over the __VA_ARGS__ approach is that it doesn't dump yet another macro on the user. If you forget the parenthesis, you can get a compile time error, so I see it as a safe solution.

Community
  • 1
  • 1
Ali
  • 56,466
  • 29
  • 168
  • 265
1

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.

Gregory Pakosz
  • 69,011
  • 20
  • 139
  • 164
0

I'm not entirely sure what you mean by a "boolean expression with commas." Simply wrapping macro expansions in commas (as seen in the samples below) protects against parse errors if you happen to use the default comma operator in your conditions, but the default comma operator does not do the same thing as &&. If you mean you want something like:

my_assert(condition1, condition2, message1, message2);

You're out of luck. How should any API tell where the conditions stop and the messages start. Just use &&. You can use the same tricks below for handling the message portion to also create a my_condition_set macro that allows you to write something like:

my_asssert(my_condition_set(condition1, condition2), message1, message2);

Getting to the message part, which is the tricky part and the most useful part that the standard assert macros tend to lack, the trick will come down to either using a custom message output type that overrides operator, (the comma operator) or to use a variadic template.

The macro uses variadic macro support and some tricks to deal with commas. Note that none of the code I'm posting here is tested directly; it's all from memory from custom assert macros I've written in the past.

The version using comma operator overloading, which works in C++98 compilers:

struct logger {
  template <typename T>
  logger& operator,(const T& value) {
    std::cerr << value;
    return *this;
  }
};

#define my_assert(condition, ...) do{ \
  if (!(condition)) { \
    (logger() , __VA_ARGS__); \
    std::terminate(); \
  } \
}while(false)

The logger() expression creates a new instance of the logger type. The list of arguments from __VA_ARGS__ is then pasted with commas separating each. These commas each invoke the comma operator left-to-right, which forwards the expression on to std::cerr. I usually use a custom log stream that handles writing to files, cerr, Windows' OutputDebugStringA, or whatever.

Using variadic templates in a C++11 compiler, this would be more like:

template <typename ...Ts>
void logger(Ts&&... argv) {
  std::cerr << your_string_format_function(argv...);
}

void logger(const char* fmt) {
  std::cerr << fmt;
}

void logger() {}

#define my_assert(condition, ...) do{ \
  if (!(condition)) { \
    logger(__VA_ARGS__); \
    std::terminate(); \
  } \
}while(false)

You'd need a format string function that actually works with variadic arguments or write an adapter for something like boost::format.

You could also use printf if you don't mind the lack of type-safety.

Sean Middleditch
  • 2,517
  • 18
  • 31