Azeirah
Azeirah

Reputation: 6338

Accessing variable values within a macro

Some time ago, I made this beautiful assert macro for c and c++ programs

#define ASSERT(truthy, message) \
     if (!(truthy)) \
     {\
         cout << message << " on line " << __LINE__ << " in file " << __FILE__ << ". Check was " << #truthy << endl;\
     }

Scatter ASSERT calls throughout your code, and it will warn you whenever the truthy value is not truthy! Very handy during development to remind you of potential mistakes.

ex

ASSERT(filesFound > 0, "Couldn't find any files, check your path!");

When filesFound is 0, the macro will print out

Couldn't find any files, check your path! on line 27 in file openFiles.c. Check was filesFound > 0

Now what I want it to print, to give me even more relevant information, is the value of any variables passed into the truthy parameter. Like this

Couldn't find any files, check your path! on line 27 in file openFiles.c. Check was filesFound > 0, filesFound is 0

This seems lisp-like territory, I wonder, is there any black magic c preprocessing that I can use to evaluate variables and functions to their values, without evaluating the truthy statement?

I assume to be disappointed.

Upvotes: 13

Views: 1742

Answers (9)

Mark
Mark

Reputation: 166

An alternative solution which I've always used is to support varargs in the macro and then force the assert user to specify the relevant message / variables - it's a little bit of extra work each time, but on the plus side you can get exactly the formatting that you want and include information not available in the "truthy" bit, e.g:

#define ASSERT(truthy, message, ...) \
if (!(truthy)) \
{\
    MyAssertHandler(__LINE__, __FILE__, #truthy, message, ##__VA_ARGS__);
}

Then you're handler is just a fairly standard var-arg function that can use e.g. vsnprintf to generate the message and output it, e.g. off the top of my head:

void MyAssertHandler(int line, const char* file, const char* expressionStr, const char* format, ...)
{
    // Note: You probably want to use vsnprintf instead to first generate
    //       the message and then add extra info (line, filename, etc.) to
    //       the actual output 
    va_list args;
    va_start(args, format);
    vprintf(format, args);
    va_end(args);

    // Log to bug database, DebugBreak() if a debugger is attached, etc.
}

usage:

ASSERT(IsBlah(), "BlahBlah: x = %.2f, name = %s", GetX(), GetName());

Upvotes: 1

Kingsley Chen
Kingsley Chen

Reputation: 385

Surprisingly, I solved a similar problem before, but I'm not sure if it could help you in this case.

The original solution was proposed by Andrei Alexandrescu in the article Enhancing Assertions, and with no question, relying on some macro tricks.

This amazing facility can be used as the following:

string s1, s2;
...
SMART_ASSERT(s1.empty() && s2.empty())(s1)(s2);

And if something goes wrong, the message would be displayed

Assertion failed in matrix.cpp: 879412:
Expression: 's1.empty() && s2.empty()'
Values: s1 = "Wake up, Neo"
        s2 = "It's time to reload."

Be noted that, the SMART_ASSERT can capture infinite variables, theoretically.

For implementation details, please check out the article.

Upvotes: 0

tony
tony

Reputation: 3887

You need to build an expression 'grabber' / builder.

The macro would become something like:

#define ASSERT_PARAM(truthy, message, param) \
 if (!(truthy)) \
 {\
     Grabber g;
     g << #truthy; // grab expression as string
     g % truthy;  // grab expression and values
     cout << message << " on line " << __LINE__ << " in file " << __FILE__ << ". Check was " << #truthy  << ", value was " << param << endl;\
     cout << g; \
 }

What does Grabber do?

It is a bunch of crazy C++ that builds up an expression. It would overload every operator to 'grab' the params of the operator. Every operator returns a reference to the grabber, so it can grab the next operator. ie

Grabber g;
g % filesFound > 0;

Since % (and * and /) have high precedence, the above parses like:

((g % filesFound) > 0)

If template<typename T> Grabber::operator%(T const & val) just records (or prints) the value passed in (ie filesFound), and - importantly - returns itself (g) so that it becomes part of the next expression: ie it becomes g > 0. Causing template<typename T> Grabber::operator>(T const & val) to be called, and > 0 to be recorded.

Then cout << g can spew out everything grabbed.

As mentioned above "It is possible — the Catch library does it. But it’s hellishly difficult".

P.S. you should wrap your macro in a do ... while 0 like this:

#define ASSERT_PARAM(truthy, message, param) \
 do \
 { \
   if (!(truthy)) \
   {\
     cout << message << " on line " << __LINE__ << " in file " << __FILE__ << ". Check was " << #truthy  << ", value was " << param << endl;\
     cout << g; \
   } \
 } while (0)

What you have currently means that this is valid code:

ASSERT(foo != 0)
else
{
}

And this is NOT valid code:

if (foo != nullptr)
   ASSERT(foo->bar != nullptr);
else
   x = 10;

Upvotes: 0

Kusalananda
Kusalananda

Reputation: 15603

I wonder if having the macro take a message is really that useful. A failed assertion is a message to the developer that there is a bug in the code that caused an exceptional behaviour or put the program in an unacceptable state. The user has less to do with it (if they even have access to the source code).

The code below defines an ASSERT macro that takes a boolean expression, evaluates it and prints an informational message. The message contains a value that you've asked to inspect upon failing the assertion.

The macro, just like the standard assert() macro (in <cassert>) goes on to call abort() (from <cstdlib>) to cause an abnormal program termination. This is what you want, because the program entered a state in which it didn't know what more to do.

I'm using std::printf() here for brevity. You do whatever you want.

#include <cstdlib>
#include <cstdio>

#define ASSERT(value, inspect)                                                 \
    if (!(value)) {                                                            \
        std::printf("ASSERTION FAILED: '%s', %s is %d: %s@%s:%d\n", #value,    \
                    #inspect, inspect, __func__, __FILE__, __LINE__);          \
        abort();                                                               \
    }

int foo() { return 42; }

int main()
{
    // ...
    ASSERT(foo() - 40 == 1, foo());
    //...
}

Program run:

$ ./a.out
ASSERTION FAILED: 'foo() - 40 == 1', foo() is 42: [email protected]:16
Abort

It's not possible to do exactly what you ask for without adding more parameters to the macro. At some point you'll have to stop and realize that you're spending time on creating a text string that you do not want to see.

Upvotes: 0

Lundin
Lundin

Reputation: 213711

Perhaps you could compromise and only allow 2 variables and 1 operator in the assertion expression? If so, you could make an ad hoc solution like this:

#include <iostream>
#include <string>

#define STRINGIFY(x) #x

#define BIN_ASSERT(obj1, op, obj2, msg)                                 \
  if(!(obj1 op obj2))                                                   \
  {                                                                     \
    std::cout << msg << " on line " << __LINE__                         \
         << " in file " << __FILE__                                     \
         << "." << std::endl                                            \
         << "Check was "                                                \
         << STRINGIFY(obj1) STRINGIFY(op) STRINGIFY(obj2)               \
         << "." << std::endl                                            \
         << "Operator " << #obj1 << ": " << obj1                        \
         << "." << std::endl                                            \
         << "Operator " << #obj2 << ": " << obj2                        \
         << "." << std::endl;                                           \
  }


int main (void)
{
  int x = 2;
  int y = 3;
  std::string s1 = "hello";
  std::string s2 = "world";

  BIN_ASSERT(1, +, -1, "Value zero"); std::cout << std::endl;
  BIN_ASSERT(x, ==, y, "Numbers not equal"); std::cout << std::endl;
  BIN_ASSERT(s1, ==, s2, "Strings not equal"); std::cout << std::endl;
}

Output:

Value zero on line 30 in file test.c.
Check was 1+-1.
Operator 1: 1.
Operator -1: -1.

Numbers not equal on line 31 in file test.c.
Check was x==y.
Operator x: 2.
Operator y: 3.

Strings not equal on line 32 in file test.c.
Check was s1==s2.
Operator s1: hello.
Operator s2: world.

Upvotes: 0

Thomas Sparber
Thomas Sparber

Reputation: 2917

I think you can split up the truthy Expression like they do it in the first answer here and then you can probably print the individual values. But I'm not sure if it actually works.

The printing could then be resulved using a variadic template function

Upvotes: 0

user1196549
user1196549

Reputation:

Maybe not the dream solution, but you can pass whole statements to a macro.

#define ASSERT(trusty, action) if (!trusty) { action }

ASSERT(trusty, cout << a << b;)
ASSERT(trusty, printf("%d, %f\n", a, b);)

Upvotes: 0

rkachach
rkachach

Reputation: 17345

What you are trying to do sounds very complicated. I'm afraid in C++ it's not possible.

Technically what you are evaluating is a bool expression so you can pass it to a parser whenever the assertion fails. The parser then will build the expression tree, get the leaves (elements of the expression) and return them. The returned values then should be printed out. To do that you will need support for reflection which is actually not supported in C++ AFAIK.

Upvotes: 0

Serge Ballesta
Serge Ballesta

Reputation: 148890

I cannot imagine a way to do it... except by passing another parameter

#define ASSERT_PARAM(truthy, message, param) \
     if (!(truthy)) \
     {\
         cout << message << " on line " << __LINE__ << " in file " << __FILE__ << ". Check was " << #truthy  << ", value was " << param << endl;\
     }

You would use it that way:

ASSERT_PARAM(filesFound > 0, "Couldn't find any files, check your path!", filesFound);

getting:

Couldn't find any files, check your path! on line 27 in file openFiles.c. Check was filesFound > 0, value was 0

Upvotes: 0

Related Questions