Assert Yourself Conditionally
How many times have you written this code:
void Fn(TYPE *val)
{
ASSERT(val);
if(val)
{
//Do some stuff
}
}
Or worse, this code:
void Fn(TYPE *val)
{
ASSERT(val && "You are about to crash!");
val->Fn();
}
Now there's a better way! Introducing the if_assert:
void Fn(TYPE *val)
{
if_assert(val)
{
//Do some stuff
}
}
Which is just defined as:
#ifdef DEBUG
#define if_assert(x) if(!!(x) || System::AssertFail(#x " " STRINGIFY(__FILE__) ":" STRINGIFY(__LINE__)))
#else
#define if_assert(x) if(x)
#endif
Some points on that code:
- You'll need to provide your own bool System::AssertFail(const char *msg) function
- It should return false.
- My System::AssertFail(...) just prints the message passed to it and does a normal ASSERT(0)
- I have this defined as extern rather than inline. That keeps the contents of System::AssertFail(...) out of the instruction cache unless the assert actually fails, which depending on the complexity of your ASSERT handler can be substantial.
- In non-DEBUG builds, this just reduces to if(x)
- The !!(x) basically means the same thing as (x), but the compiler doesn't complain about automatic conversion to bool
- Because C uses lazy evaluation, nothing after the || will get evaluated if (x) is true
- STRINGIFY just converts its argument to a string. There is some magic happening there, though, because you want the compiler to convert __FILE__ to a string before the macro converts it to the string (otherwise you just get "__FILE__" instead of the actual file name), so:
- #define _STRINGIFY(x) #x
- #define STRINGIFY(x) _STRINGIFY(x)
- Unfortunately, this doesn't support declaring variables inside the if_assert:
- Do:
- TYPE *val = GetValue();
- if_assert(val) ...
- not:
- if_assert(TYPE *val = GetValue()) ...
- though if you're dong this, you probably don't need an ASSERT:
- if(TYPE *val = GetValue()) ...
Hey Craig!
ReplyDeleteWhat about void Fn(TYPE &val)?
In other words, make the caller do the check. To me, pointer means optional and if we're putting in an assert it's clearly not optional, which makes for a confusing interface.
Well, that's strictly example code of course. :)
ReplyDeleteI've typically been doing this in cases where one component is expecting another component to be present on the same game object. There's a pointer that needs to be bound to the other component well after construction, so it can't be a reference.
This basically lets me know "the data is bad" without crashing the game, so I can fix it and reload it without restarting the game.