Asserts are awesome. They are one item in a programmer’s toolkit that can be seriously under-utilized. I have found them to be the most effective way to detect and fix erroneous results in my day-to-day programming. In this post I am going to be exploring a few ways we can make our own assert macro a little bit more robust.
Note – Moving Forward with MSVC
Using preprocessor directives often varies from compiler to compiler. In this example I am using Microsoft’s VC++ in VisualStudio 2013 but the features I will be using are supported in one way or another in any compiler worth using nowadays.
Naive Assertions
Most programmers can come up with a simple assert macro that works for them. A first go at it might look something like this:
[cpp]
#define ASSERT(x) if(!x) { __debugbreak(); }
[/cpp]
This is pretty straight forward. If x does not evaluate to true, then we will call the __debugbreak function. This logic almost immediately fails in real-world usage though. Let’s walk through some good practices when dealing with assertion macros.
Parenthesize macro arguments
Consider the following test which checks two values or’d together:
[cpp]
ASSERT(true || false);
[/cpp]
This should always evaluate to true, and therefore not cause an assertion, but with our current assert it will fail. It is clear when you look at how the macro expands:
[cpp]
if(!true || false) { __debugbreak(); }
[/cpp]
This is were you learn a lesson applicable to most macros you will ever write: Always parenthesize your macro arguments. After adding parenthisis our macro now takes complex conditionals with ease and correctness:
[cpp]
// Note the parenthesis in our if check
#define ASSERT(x) if(!(x)) { __debugbreak(); }
// Statement now expands to:
if(!(false || true)) { __debugbreak(); }
[/cpp]
Wrap macro logic in do-while loops
Writing a macro with any logic can be tricky. If you’re not careful, debugging macros can leave you scratching your head for hours. This next method can help alleviate some of those situations.
Let’s look at the following situation:
[cpp]
if(entity.IsPlayer())
ASSERT(entity.IsAdmin());
else
entity.Kill();
[/cpp]
Logically, this code is sound:
- If the entity is a player, assert that they are an admin.
- If the entity is not a player, kill them.
While you wont get any warnings or errors, this logic is contorted a bit by our expanded macro:
[cpp]
if(entity.IsPlayer())
if(!entity.IsAdmin()) { __debugbreak(); }
else
entity.Kill();
[/cpp]
Our macro subtly twists our logic against us. By wrapping our macro logic in a do{ .. }while(0), we can stop this from happening:
[cpp]
// Newlines added for readability
#define ASSERT(x) do { \
if(!(x)) { __debugbreak(); } \
} while(0)
// Logic now expands to:
if(entity.IsPlayer())
do {
if(!(entity.IsAdmin())) { __debugbreak(); }
} while(0);
else
entity.Kill();
[/cpp]
Disable the ASSERT macro in release builds
Assertions are great because they are a quick and easy way to ensure that your program isn’t using erroneous data, but you don’t always want them. Large projects that have hundreds of thousands of lines of code will have a lot of assertions (especially if you’re following rule #5 when writing safety critical code for NASA), so wouldn’t it be nice to have the ability to disable them? Luckily, because our assertions are in a macro, we can disable them, leaving us with virtually no overhead from them.
Using preprocessor directives we can toggle our assertions with a #define like so:
[cpp]
#ifdef _USE_ASSERTS
#define ASSERT(x) do{ \
if(!(x)) { __debugbreak(); } \
} while(0)
#else
#define ASSERT(x) // Do nothing with X
#endif
[/cpp]
Let’s compile with asserts disabled using some of our previous code:
[cpp]
const bool success = entity.Kill();
ASSERT(success);
[/cpp]
You will most likely get a compiler warning somewhere along the lines of this:
[cpp]warning C4189: ‘success’ : local variable is initialized but not referenced[/cpp]
While the best option would be to remove/rework code like this, one way to get around this is by casting your macro argument to void like so:
[cpp]
// Note that we still use a do-while
#define ASSERT(x) do { (void)(x); } while(0)
[/cpp]
While this does removed the ‘unused variable’ warning for most compilers, it can still have side-effects. For example, if we are asserting the return value of a function directly, it will still call this function. A nifty trick to get around that is to use the sizeof operator on your argument:
[cpp]
// Note that we still use a do-while
#define ASSERT(x) do { (void)sizeof(x); } while(0)
[/cpp]
This works because the sizeof operator is guaranteed to not evaluate the arguments to it. This should also allow the compiler to optimize the small remains of our disabled assertions.
Note – Executing needed code in an assertion
When we disable our asserts, the value inside of the assert is no longer evaluated or executed. If we changed the code snippit above to not use a local variable, our code would not execute as planned:
[cpp]
ASSERT(entity.Kill());[/cpp]
This entire line would be skipped. You will inevitably make this mistake and experience this behavior when switching to a release build and then you will never make this mistake again.
Spiff up your results
Up to this point we have been under the assumption that we are always going to be attached to a debugger while running the game, so just calling __debugbreak is sufficient. If ever want to get info from an assert when not actively attached to a debugger, we’ll need to add some output to tell us what went wrong. Luckily for us most compilers have predefined macros that will help us report this info very easily.
The first thing we will want to do is change our behavior when we are asserting. I’ve always found it easiest to call a function in the case of an assert so you can keep the size of your macro to a minimum. Something like this will do nicely:
[cpp]
namespace Assert
{
void HandleAssert(const char *message,
const char *condition,
const char *fileName,
long lineNumber);
}
[/cpp]
In the definition for our HandleAssert function we can do pretty much any logging we want. I personally prefer a a pop-up message box with some information, but here is a bare-bones version that works just fine:
[cpp]
void Assert::HandleAssert(const char *message, const char *condition, const char *fileName, long lineNumber)
{
std::cerr << "Assert Failed: \"" << message << "\"" << std::endl;
std::cerr << "Condition: " << condition << std::endl;
std::cerr << "File: " << fileName << std::endl;
std::cerr << "Line: " << lineNumber << std::endl;
std::cerr << "Application now terminating";
}
[/cpp]
Now we must fix our macro to call HandleAssert. We will be using a few predefined macros for this:
- __FILE__ – Current file we are in.
- __LINE__ – Current line we are on.
- #x – Stringify ‘x’. This allows us to display the exact conditional that caused our assertion.
A full list of predefined macros for MSVC can be found here.
Using these macros, our assert starts to look something like this:
[cpp]
#ifdef _DEBUG
#define ASSERT(x) do{ \
if(!(x)) {\
Assert::HandleAssert(“Assertion!”, #x, __FILE__, __LINE__); \
__debugbreak(); \
}\
} while(0)
#else
#define ASSERT(x) do { (void)sizeof(x); } while(0)
#endif
[/cpp]
Note that we left our __debugbreak call outside of our helper function. This will cause the compiler to break on the line we put our assert on instead of breaking inside of our helper function.
Another useful option is to add specific messages to our asserts. This is trivial to do in our case because we have an additional parameter that we pass to our HandleAssert function. The cleanest was to do this is to create another macro that accepts two arguments: one for the conditional and one for the message to display if the assert fails. Here is our entire macro which includes our newly added custom message assert ASSERT_MSG:
[cpp]
#ifdef _DEBUG
#define ASSERT_MSG(x, msg) do{ \
if(!(x)) {\
\
Assert::HandleAssert(msg, #x, __FILE__, __LINE__); \
__debugbreak(); \
}\
} while(0)
#define ASSERT(x) do{ \
if(!(x)) {\
Assert::HandleAssert(“Assertion!”, #x, __FILE__, __LINE__); \
__debugbreak(); \
}\
} while(0)
#else // Disabled Asserts
#define ASSERT(x) do { (void)sizeof(x); } while(0)
#define ASSERT_MSG(x, msg) do { (void)sizeof(x), (void)sizeof(msg); } while(0)
#endif
[/cpp]
Go forth and assert
With this setup you’ll have a pretty decent assert macro for you to use. It is also pretty easy to add extra features as needed. Here are a few I’ve used in the past that can be helpful:
- Define a different definition for HandleAssert based on what type of build you are using. Shipping builds should probably use a graceful crash handler/error reporter, while debug should give you the dirty details.
- Make the macros cross-platform. It is easy to extract the needed macros (file/line number, debug breaking) into their own user-defined macros which you can define for each platform you build on.
- Allow for different assertion behavior. You can have HandleAssert return a value in which you specify whether or not you want the process to halt. This can be helpful if you want to give the user an option to try to continue running the process. This is generally discouraged because you’ve probably already entered undefined behavior, so a crash is likely anyways.
Here is the source for the completed example if you’re interested: Assert.zip
Leave a comment if you have any other cool tricks with asserts. They’re silly looking and stupid-easy to write, but damn are they awesome.
Read More