Assert Sucks

Published January 09, 2008
Advertisement
Wow, it's been a long time since I did an entry. Life's been busy, although it's also been better than I can ever remember it being. I have some issues to work on in terms of balancing all the different pieces, but overall I have to say I'm really happy with things.

Assert Sucks

If you do work in C/C++, you're almost certainly familiar with the assert macro and asserts in general. It's unfortunate, though, because although the standard library gives you assert simply by including a simple header, what it provides you with is almost completely worthless. What you get is a macro which changes its definition based on whether NDEBUG is specified. In debug builds, it checks the condition and provides some kind of notification (including debugger notification) if the condition is not satisfied. In release builds, it does absolutely nothing. That's great, until you start writing sizable software and realize just how idiotic this is.

There's a lot of problems with this arrangement. First of all, whether or not asserts are enabled has probably been linked to several other settings as a result of using NDEBUG:

* Is optimization enabled?
* Are symbols available?
* What versions of other libraries are being linked?
* What floating point model is in use?
* Is RTTI enabled?
* Are exceptions enabled?

These are all totally irrelevant, and in fact there's a lot of cases where you for god's sake want asserts to be enabled regardless of what those values are. (This gets to be real fun around the time the optimizer starts breaking your code.) Asserts should be on their own flag for enabled or not, completely separate of everything else. That way, you can have optimized (ie, playable speed) release builds that won't explode with zero indication of what the hell happened. Or, you can check that basic assumptions of your math are correct regardless of what floating point model is currently set.

Then there's the matter of what exactly should happen to an assert in various configurations. There are a lot of situations where you don't actually want to remove asserts, but actually convert them to exceptions or longjmps (I know, I know) or something. There's any number of reasons for this, but basically it boils down to the fact that most assert conditions are ridiculously cheap to check, and no software is perfect. If you've used asserts liberally -- which you should -- then this provides sort of a last ditch protection and debugging mechanism you can use when your customer comes to you with a crash and no information apart from a stack dump.

Different asserts will generally vary in importance, too. It's not unusual to verify on data errors in a console game, even though that'd be really poor form in a PC game. You might want to treat code and data errors differently, log them to different streams, ignore them in some cases, etc. It becomes useful to have different classes of asserts which are linked into the logging and error handling subsystems in different ways. Depending on the type of build, it can also be useful to control each category of assert with a separate define. In short, you need your own asserts. It's not just that the library assert is badly done. No library assert will ever be adequate, because it will either be too simple to be useful or too complex to be usable. Any sizable code base should have its own custom assert implementation that is tailored to the needs of that codebase and its engineers.

Luckily, writing an assert implementation is really easy. You can pretty much just start by copying the library assert and then making tweaks to it from there. (It is kind of important to start with the library implementation, though. It's written funny because of some important subtleties that you're unlikely to realize on your own.) The basic idea is that you have an assert macro which is going to call an assert function when enabled. You'll probably throw in some niceties with __FILE__ and other similar preprocessor directives. The actual assert function is usually a free function that ties into a global logging system and does the most appropriate thing. (Logging is one of those things that looks more and more attractive as a straight up honest to god global as days go by.) After that it's just patching in whatever features you want from your assert, most of which take trivial amounts of effort to actually implement.
0 likes 2 comments

Comments

rollo
Yeah, I think everybody ends up writing their own asserts. If anyone wants to do so, this is a good starting point (it will illustrate those tricky details you mentioned): Stupid C++ Tricks: Adventures in Assert.
January 09, 2008 04:58 PM
dcosborn
Good article! I definitely agree that the tendency to lump all debugging features together, not to mention unrelated compiler configuration details, is horribly wrong. The trick is to come up with a sensible categorization of compiler and feature switches that allows you to get flexible customized builds without being overloaded with options.

I recently fixed (partially) a similar issue in my project. My build system allows you to cumulatively select debugging with a single command-line switch, --enable-debug, which affects compiler debug and optimization flags, the state of the NDEBUG macro for assert, as well as the state of a DEBUG macro which affects certain features in the code, such as prefixing file/line information to exception strings when throwing. As you point out, sometimes you want a mixture, such as assertions in an optimized build. Additionally, building my program with compiler debugging enabled takes much longer than without (30+ seconds to link versus 2 seconds without), leading me to eventually disable debugging entirely during most active development (which means coding without assertions! [wow]). For these reasons, I added an --enable-debug=partial switch which disables compiler debugging (including assert), enables compiler optimization, and enables code debugging features.

When I released my first demo before Christmas, I didn't have this partial-debug switch and therefore I didn't get file/line information in the exception strings when it crashed on other people's machines, making it harder to track down the bugs.

After reading your post, I think I should add an --enable-assert switch since, as you say, assertions aren't slow and help to catch bugs early. Your assert-that-throws is a great idea too; it could work well as an --enable-assert=throw switch.
January 09, 2008 07:24 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement