Next article: Friday Q&A 2013-05-17: Let's Build stringWithFormat:
Previous article: Friday Q&A Is On Vacation
Tags: assert c cocoa fridayqna objectivec
Asserts are a powerful tool for building quality code, but they're often poorly understood. Today, I want to discuss the various options for writing asserts in Cocoa apps and the best ways to use them, a topic suggested by reader Ed Wynne.
APIs
Fundamentally, an assert is just a call that takes an expression and indicates failure in some way if the expression isn't true. The basic idea is to check for conditions that should always be true so that you fail early and obviously, rather than failing later and confusingly. For example, an array dereference will fail in various weird ways if you give it a bad index:
x = array[index]; // sure hope index is in range
Using an assert can help make it obvious what went wrong:
assert(index >= 0 && index < arrayLength);
x = array[index];
This actually demonstrates an API for asserts. C provides the assert
function if you #include <assert.h>
. It takes a single expression. If the expression is true, it does nothing. If it's false, it prints the expression, the file name and line number where the assert is located, and then calls abort
, terminating the program.
Cocoa provides several assert functions as well. The most basic is NSAssert
. It takes an expression and a string description, which can be a format string:
NSAssert(x != y, @"x and y were equal, this shouldn't happen");
NSAssert(z > 3, @"z should be greater than 3, but was actually %d", z);
NSAssert(str != nil, @"nil string while processing %@ of type %@", name, type);
Like the assert
function, this logs the assertion failure if the expression is false. It then throws an NSInternalInconsistencyException
, and what happens then depends on what exception handlers are present. In a typical Cocoa app, it will either be caught and logged by the runloop, or it will terminate the application.
Unfortunately, the logging from NSAssert
is weak. It logs the fact that the assertion failed, as well as the method it was in and the filename and line number, but it doesn't actually log the expression that failed, nor does it log the reason string provided to the macro. The exception it throws does include the reason string, at least, so as long as the exception gets printed at some point, that will show up.
There are a few variants of this call available in Cocoa. The NSAssert
call only works within an Objective-C method, so there's an equivalent NSCAssert
call that works in a C function. There's also NSParameterAssert
, which doesn't take a description string and is intended for quickly checking a parameter value, and an equivalent NSCParameterAssert
for C functions.
Build Your Own
The built-in options aren't great. The C assert
is decent, but doesn't allow for a customizable message. The Cocoa calls have bad logging, and their behavior in the event of a failed assertion depends too much on runtime context, and may not actually terminate the app.
These things aren't hard to build, though, so let's build one that does things right! We'll want a call that takes an expression and optionally a description format string:
MAAssert(x > 0);
MAAssert(y > 3, @"Bad value for y");
MAAssert(z > 12, @"Bad value for z: %d", z);
It should log the expression, the format string if it exists, and the filename, line number, and function name where the problem occurred. Additionally, it should only evaluate the format string parameters if the assertion fails, to make things more efficient. All of this calls for a macro.
Like all good multi-line macros, this macro is wrapped in a do
/while
construct:
#define MAAssert(expression, ...) \
do { \
The first thing it does is check whether the expression is actually false:
if(!(expression)) { \
If it is, it uses NSLog
to log the details of the failure:
NSLog(@"Assertion failure: %s in %s on line %s:%d. %@", #expression, __func__, __FILE__, __LINE__, [NSString stringWithFormat: @"" __VA_ARGS__]); \
The #expression
construct produces a string literal containing the text of the expression. For example, it will produce "x > 0"
for the first assert call above. The __func__
identifier produces the name of the current function. __FILE__
and __LINE__
should be self-explanatory. The dummy @""
in the stringWithFormat:
call ensures that the syntax is legal even when no reason string is provided.
After logging the assertion failure, it then terminates the app by calling abort
, and the macro ends:
abort(); \
} \
} while(0)
This works perfectly. It allows an additional explanatory string, but doesn't require it for cases where the expression is enough to make it clear what's going wrong. It always calls abort
on failure, rather than throwing an exception that could potentially be caught. It logs all available details at the point of failure.
Application Specific Information
It would be great if we could get the failure message to show up in crash logs as well. Turns out, we can! Wil Shipley demonstrated how to put custom data into the "Application Specific Information" section of a crash log. Put this somewhere in the source code:
const char *__crashreporter_info__ = NULL;
asm(".desc ___crashreporter_info__, 0x10");
Any string written into this magic global variable will show up in that section of the crash log. This doesn't work everywhere (word is that it doesn't work on iOS), but it can be handy, and does no harm when it doesn't work. If you want to take advantage of this, a small modification to the assert macro will put the message into this variable as well as logging it:
#define MAAssert(expression, ...) \
do { \
if(!(expression)) { \
NSString *__MAAssert_temp_string = [NSString stringWithFormat: @"Assertion failure: %s in %s on line %s:%d. %@", #expression, __func__, __FILE__, __LINE__, [NSString stringWithFormat: @"" __VA_ARGS__]]; \
NSLog(@"%@", __MAAssert_temp_string); \
__crashreporter_info__ = [__MAAssert_temp_string UTF8String]; \
abort(); \
} \
} while(0)
And, as if by magic, the message appears in the crash log.
Philosophy
Now that you know how to write an assert in many different ways, just what kind of asserts should you write?
Asserts should be written for conditions that, according to your understanding of the program, should never occur. Asserts should not be used to check for errors that are actually expected to happen in some cases. For example, asserting that a filename is not nil
is good technique:
assert(filename != nil);
However, asserting that data could be read from that file is bad practice:
NSData *data = [NSData dataWithContentsOfFile: filename];
assert(data != nil);
That call can legitimately fail due to real-world conditions, such as the file not existing on disk, or not having permissions to read it. Because of that, this code needs some actual error handling, not just an assert. Failing to read the file should result in taking an alternate approach or alerting the user that something went wrong, not just logging and terminating the app.
Typically, the most useful place for asserts is at the top of a function or method, to check constraints on the parameters that can't be expressed in the language directly. These asserts correspond directly to constraints expressed in the documentation. For example:
// Flange an array of sprockets. The sprockets array must contain
// at least two entries, and the index must lie within the array.
- (void)flangeSprockets: (NSArray *)array fromIndex: (NSUInteger)index
{
assert([array count] >= 2);
assert(index < [array count]);
...method body...
The gap between a caller and a callee makes it easy to lose track of these constraints, making this an excellent place to double-check that everything is as it should be. Special attention should be paid to parameters that are easy to screw up, and to parameters where bad values will cause strange failures. For example, this assert checking for NULL
, while still useful, doesn't add much, since the resulting crash without it would still be fairly clear:
assert(ptr != NULL);
x = *ptr;
It's not bad, but your time may be better spent elsewhere. This assert checking for nil
is really handy, as a nil
value here will just result in a strangely built string, which could show up far away and much later:
assert(name != nil);
str = [NSString stringWithFormat@"Hello, %@!", name];
It can also be handy to add asserts in the middle of complex code which has clear pre or post-conditions. For example, in the middle of modifying a data structure, you might check to make sure all of your variables have consistent values between themselves:
assert(done + remaining == total);
This will let you catch logic errors quickly.
Avoid asserts for obvious conditions that have little room for error. For example, these are pointless:
int x = 1;
assert(x == 1);
for(int i = 0; i < 10; i++)
{
assert(i >= 0);
...
There's no way these asserts will fire unless the computer is seriously malfunctioning, so they're basically a waste of time. Concentrate on things that "can't happen" if parts of your program work together as they should, but that you could conceivably miss.
Finally, make sure that the conditions you're asserting are reasonably fast to evaluate. You don't want them bogging down your program. Don't loop through your million-element array asserting a complex condition on every entry just out of paranoia.
In short, assert essential preconditions of your code, with an eye toward things that will cause you pain if not caught early. The goal is to get a leg up on debugging when things start to go wrong.
Disabling Asserts
If you search the web for information about asserts, you'll invariably turn up discussions of how to disable asserts in your release builds. Most assert systems have a way to disable asserts program-wide. For the C assert
call, setting the NDEBUG
macro disables it. For the Cocoa assert calls, setting the NS_BLOCK_ASSERTIONS
macro disables them. There are generally two reasons given for disabling asserts in release builds:
- Asserts impose a runtime cost that you shouldn't make every user pay. In theory, if you've tested thoroughly, you shouldn't encounter any assertion failures in your release builds anyway.
- An assertion failure immediately terminates the app, which users don't like. By removing asserts, you give the program a chance to continue functioning in the face of a bug.
However, I am firmly of the opinion that disabling asserts in release builds is a terrible idea. The runtime cost should be negligible, and if it's not, then you should redo your asserts to fix that. As for avoiding app termination, asserts should be written such that a failure always means that something has gone terribly wrong. It is possible that the app will continue functioning in the face of that. It's more likely that it'll crash. It's also possible that it'll keep running, but corrupt your user's data. A clean crash is vastly preferable. No code is free of bugs, and crashing early and obviously when a bug is encountered is much better, even in a release build running on a user's machine. Generating a cleaner crash log will help you debug the failures more quickly.
The example MAAssert
macro above doesn't have any built-in way to disable it for this reason. If you use a different assert facility, I strongly recommend that you avoid ever turning them off.
Conclusion
Asserts are a valuable tool for producing better code and making bugs easier to find and fix. Asserts should be used anywhere there's a constraint on a value that isn't enforced by the language. My general guideline is that if you document a restriction for callers, you should also assert it in the code. If you ever find yourself writing some code that gets you thinking a lot, and has variables whose values should relate to each other in a certain way not enforced by the language, assert that in the code.
That's it for today. Friday Q&A is driven by reader suggestions as always, so if there's a topic that you'd like to see covered here, please send it in. Come back soon for another exciting article.
Comments:
assert(ptr != NULL && "Invalid pointer");
to provide a custom message with the assert failure.
NSAssert(foo, @"foo wasn't supposed to be nil");
if (!foo) {
return; // don't do anything
}
// do something
In production code show up a warning to let the user know what has happened.
In debug builds just crash ;-)
Patrick aka Jolly
In order to remember, I like to think of it as if a block were a C function (not far from the truth) instead of an ObjC method.
This is what I usually used NSException for. Check using an if statement, raise an NSException if true. Having read this post, asserts seem to be the better solution. While it is clear to me when to use NSError (thanks to http://www.jayway.com/2010/10/13/exceptions-and-errors-on-ios, I'm left wondering when to use NSException?
Any good guideline when to use NSException?
Cheers
Simon
const char *__crashreporter_info__ = NULL;
asm(".desc _crashreporter_info, 0x10");
I wanted to put them in a header, but that didn't work out (duplicate symbols error). Can I put them into the macro?
@me: That's mysterious, that asm line seems to work either way, with 3 underscores at the start and 2 at the end, or 1 at the start and 0 at the end.
The library is called BrynKit (it's basically a personal and informally-maintained thing, although if people like the logging and debugging tools, I might break them out into a separate library/pod). It's available here: http://github.com/brynbellomy/BrynKit
The assertion macros themselves are in https://github.com/brynbellomy/BrynKit/blob/master/Main/BrynKitDebugging.h
I'd love any feedback or contributions anyone might want to offer, as I'm sure the library could stand to mature quite a bit ;) Hope it's useful to someone out there.
__crashreporter_info__
string as described here doesn't work in stripped builds. The asm directive is allegedly supposed to fix that, but in my experience it doesn't. What does seem to work is to declare the string external, i.e., extern const char* __crashreporter_info__;
instead of declaring your own global variable. Or you could get it dynamically as is done here: http://jens.ayton.se/blag/hackin-ur-crash-reportz/Comments RSS feed for this page
Add your thoughts, post a comment:
Spam and off-topic posts will be deleted without notice. Culprits may be publicly humiliated at my sole discretion.