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 theassert 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 callsabort, terminating the program.
Cocoa provides several assert functions as well. The most basic isNSAssert. 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 theassert function, this logs the assertion failure if the expression is false. It then throws anNSInternalInconsistencyException, 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 fromNSAssert 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. TheNSAssert call only works within an Objective-C method, so there's an equivalentNSCAssert call that works in a C function. There's alsoNSParameterAssert, which doesn't take a description string and is intended for quickly checking a parameter value, and an equivalentNSCParameterAssert for C functions.
Build Your Own
The built-in options aren't great. The Cassert 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 ado/while construct:
#defineMAAssert(expression,...) \do{ \
The first thing it does is check whether the expression is actually false:
if(!(expression)){ \
If it is, it usesNSLog to log the details of the failure:
NSLog(@"Assertion failure: %s in %s on line %s:%d. %@",#expression,__func__,__FILE__,__LINE__,[NSStringstringWithFormat:@""__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 thestringWithFormat: 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 callingabort, 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 callsabort 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:
constchar*__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:
#defineMAAssert(expression,...) \do{ \if(!(expression)){ \NSString*__MAAssert_temp_string=[NSStringstringWithFormat:@"Assertion failure: %s in %s on line %s:%d. %@",#expression,__func__,__FILE__,__LINE__,[NSStringstringWithFormat:@""__VA_ARGS__]]; \NSLog(@"%@",__MAAssert_temp_string); \__crashreporter_info__=[__MAAssert_temp_stringUTF8String]; \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, justwhat kind of asserts should you write?
Asserts should be written for conditions that, according to your understanding of the program, shouldnever occur. Asserts shouldnot be used to check for errors that are actually expected to happen in some cases. For example, asserting that a filename is notnil is good technique:
assert(filename!=nil);
However, asserting that data could be read from that file is bad practice:
NSData*data=[NSDatadataWithContentsOfFile: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*)arrayfromIndex:(NSUInteger)index{assert([arraycount]>=2);assert(index<[arraycount]);...methodbody...
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 forNULL, 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 notbad, but your time may be better spent elsewhere. This assert checking fornil is really handy, as anil value here will just result in a strangely built string, which could show up far away and much later:
assert(name!=nil);str=[NSStringstringWithFormat@"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:
intx=1;assert(x==1);for(inti=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 Cassert call, setting theNDEBUG macro disables it. For the Cocoa assert calls, setting theNS_BLOCK_ASSERTIONS macro disables them. There are generally two reasons given for disabling asserts in release builds:
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 ispossible 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 exampleMAAssert 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, pleasesend it in. Come back soon for another exciting article.
assert(ptr != NULL && "Invalid pointer");
NSAssert(foo, @"foo wasn't supposed to be nil");
if (!foo) {
return; // don't do anything
}
// do something
const char *__crashreporter_info__ = NULL;
asm(".desc _crashreporter_info, 0x10");
__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/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.