Skip to content

Instantly share code, notes, and snippets.

@iamalbert
Last active June 5, 2022 04:51
Show Gist options
  • Save iamalbert/de463e388173ca6f862c2a730fb64cee to your computer and use it in GitHub Desktop.
Save iamalbert/de463e388173ca6f862c2a730fb64cee to your computer and use it in GitHub Desktop.
C++ SMART ASSERT
#define __ASSERT1__(x) __REPORT__(x).__ASSERT2__
#define __ASSERT2__(x) __REPORT__(x).__ASSERT1__
#define __REPORT__(x) report(#x,(x))
#define ASSERT(cond, msg) if(!(cond)) Assert(#cond,msg,__FILE__,__LINE__).__ASSERT1__
#include <iostream>
struct Assert {
template<class T>
Assert(const char * cond, T && msg, const char * file, int line) {
std::cerr << "Assertion Failed: " << cond << "\n";
std::cerr << "Message : " << msg << "\n";
std::cerr << "File : " << file << ", line " << line << "\n";
}
template<class T>
Assert & report(const char * name, T&& value){
std::cerr << " " << name << ": " << value << "\n";
return *this;
}
struct Empty {};
static constexpr Empty __ASSERT1__ {}, __ASSERT2__ {};
~Assert(){ exit(-1); }
};
#include "Assert.hpp"
int main(){
int a = 1, b = 2;
std::cout << sizeof(Assert) << "\n";
ASSERT(a + b == 4, "addition")(a)(b);
}
@iamalbert
Copy link
Author

iamalbert commented Apr 5, 2021

I am surprised someone notices this 5-year old post. :)

This is a hacky way to do infinite expansion of macros by abusing the preprocessor. If you're new to C++, you don't need to take this trick too seriously.

Better assertion message

ASSERT(a + b == 4, "addition");

expands into (by the definition at L5)

if(!(a + b == 4)) Assert("a + b == 4","addition" , __FILE__ , __LINE__).__ASSERT1__;

So you can see, if a + b == 4 is evaluated to false, an Assert instance gets constructed, which prints the error message like following

Assertion Failed: a + b == 4                                                                                                                                                                
Message         : addition                                                                                                                                                                  
File            : main.cpp, line 33  

.__ASSERT1__ is nothing but accessing a static member of Assert. It does nothing and compiler will just eliminate it during optimization. Actually, the __ASSERT1__ member can be of any type. Defining Empty struct is just my personal preference.

Print debug info

Sometimes if assertion fails, we need to print some expression for debugging purpose. Here we wish it to print the value of a+1 in case assertion fails:

Assertion Failed: a + b == 4                                                                                                                                                                
Message         : addition                                                                                                                                                                  
File            : main.cpp, line 33                                                                                                                                                         
  a+1: 2       

We decide to "invent" the syntax like this:

ASSERT(a + b == 4, "addition")(a+1);

which expands into (by the definition at L5)

if(!(a + b == 4)) Assert("a + b == 4","addition" , __FILE__ , __LINE__).__ASSERT1__(a);

Here __ASSERT1__(a) is no longer accessing a member, but a macro. This is why we need to force macro and static member to have the same names, and probably why it looks so confusing.

The macro further expands into (by the definition at L1)

if(!(a + b == 4)) Assert("a + b == 4","addition" , __FILE__ , __LINE__).__REPORT__(a+1).__ASSERT2__;

which further expands into (by the definition at L2)

if(!(a + b == 4)) Assert("a + b == 4","addition" , __FILE__ , __LINE__).report("a+1", (a+1) ).__ASSERT2__;

Here .report("a+1", (a+1) ) will print the debug information we want, and then return the Assert instance itself. Therefore, .__ASSERT2__ is simply accessing its static member and will be treated as a no-op by compiler again.

More expressions

It's called "smart assert" because you can append infinite number of expressions after ASSERT(...) as long as you wrap them in parenthesis.

For example,

ASSERT(a + b == 4, "addition")(a+1)(b);

expands into

if(!(a + b == 4)) Assert("a + b == 4","addition" , __FILE__ , __LINE__).report("a+1", (a+1) ).__ASSERT2__(b);

We got __ASSERT2__(b) as a macro again. This line will expand into

if(!(a + b == 4)) Assert("a + b == 4","addition" , __FILE__ , __LINE__).report("a+1", (a+1) ).report("b", (b)).__ASSERT1__

and then it prints

Assertion Failed: a + b == 4                                                                                                                                                                
Message         : addition                                                                                                                                                                  
File            : main.cpp, line 36                                                                                                                                                         
  a+1: 2                                                                                                                                                                                    
  b: 2  

Why mutual recursion?

You might want to ask, why do we have to use mutual recursion? It seems a simple recursion could do the expansion. However,

#define __ASSERT1__(x) __REPORT__(x).__ASSERT1__
#define __REPORT__(x) report(#x,(x))
#define ASSERT(cond, msg) if(!(cond)) Assert(#cond,msg,__FILE__,__LINE__).__ASSERT1__

ASSERT(a + b == 4, "addition")(a)(b)

will expand into

if(!(a + b == 4)) Assert("a + b == 4","addition","test.cpp",36).report("a",(a)).__ASSERT1__(b);

and then the expansion stops here.

This is a indirect Self-Referential Macro and __ASSERT1__(b) will not continue to expand. It finally leads to a compile error as there is no operator()(int) defined in Assert::Empty.

test.cpp:36:40: error: no match for call to ‘(const Assert::Empty) (int&)’
   36 |     ASSERT(a + b == 4, "addition")(a)(b);
      |                                        ^

So we have no choice but to use mutual recursion to work around.

In conclusion, by mutual recursion of between macros __ASSERT1__ and __ASSERT2__, and by two useless members Assert.__ASSERT1__ and Assert.__ASSERT2__, we manage to coin a confusing syntax and call it a "smart" assert, which is usually builtin function in any sane programming language. C macros doesn't make any sense. It's a legacy tool and doesn't worth beginners' time.

@ChuckStarchaser
Copy link

ChuckStarchaser commented Apr 5, 2021

That's brilliant! I found this page by searching for smart_assert, since a very old backup of a suchly named boost folder that I have was not in the current boost, or at least I could not find it; but now I had a second look at that and it seems to be a totally different smart_assert by somebody else. I could not understand that other code either, but due to the sheer size of it, rather; I much prefer yours; and I think I'm going to use it. One feature I wish it had is a way to stop execution, like debug break, rather than abort.
A rather crazy thing I've been thinking about is having targets ALPHA and BETA, between DEBUG and RELEASE, that change the behavior of asserts to logging assertion failures and continue running (with or without "fixing" things). For example, if an addition should never overflow, but saturating the result would be a preferable compromise to shutting down the program, in DEBUG mode, the assert would stop execution; in ALPHA it would log the error to a file and ask the user whether to continue; in BETA it would log silently and continue; and in RELEASE mode it would just saturate the result and continue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment