Posted on | December 10, 2010 | 1 Comment
I’ve spent the better part of two days tracking down what turned out to be a very stupid and avoidable bug in a library (that shall remain nameless).
Examine the following piece of seemingly innocent code:
str = realloc(str, len); assert(str != NULL);Some of you are probably howling at your monitor by now. Guess what, there’s more, ‘len’ turned out to be a signed integer.
When you build a production release of something, you usually do it with NDEBUG defined, which also turns off assertions completely. So, you guessed it .. passing a signed integer to realloc() resulted in failure, which wasn’t handled because nothing was asserted. Additionally, various incarnations of realloc() just return the original pointer if they weren’t able to allocate a new one and copy the contents of the old.
When you language has facilities to check for failures, you should probably be checking. If, in my example ‘str’ was checked for being NULL and a meaningful error was reported if that was the case, I would not have needed to dig through a mountain of someone else’s library code. A lot of programmers use preprocessor directives to be much more verbose if NDEBUG is not set, that is why we have NDEBUG.
This is why assertions got a bad reputation. People use them as a quick and easy way to handle errors that are actually likely to happen, rather than to assert something they feel is impossible to happen. This is what Google was talking about when they refused to include assertions in “Go”.
This particular bug was only tickled under very strange circumstances, which is why the unit tests that shipped with the library continued to pass, even without assertions. Three simple lines of code exposed the actual bug, and the larger issue surrounding it (a lack of type safe callbacks, which is why gcc didn’t pick up on the signedness difference, as len was set from a union cast as void).
Please, don’t assert() unless you are sure that the condition could only happen if the universe reversed itself. Good uses are:
- Asserting a member of a structure is in a particular place (legacy, but sometimes needed)
- Asserting that a prime generator actually returns a prime
- Asserting value when dealing with signed integers
- Asserting compiler built in behavior, for instance making sure abs() works
- Asserting a hashing algorithm remains endian clean
In other words, if it (could) fail, check for that and deal with the error appropriately.