« Security notes, Red Hat Enterprise Linux and rssh | Main | Locking, double-checked locking and speed »
January 26, 2006
Objective-C enumeration macro
I posted a short article the other day on double-checked locking and in the process of trying to get to the bottom of the memory barrier question I found myself reading this article by Jonathan Rentzsch (also published on the IBM site in modified form). One thing leads to another and I found myself reading his love/hate paper on Objective-C, then this one on Objective-C enumeration and finally that took me to Michael Tsai's article on the same subject.
Well, I'll throw my hat in the ring too...
First up, I share Jonathan's and Michael's discontent with the standard enumeration idiom. It's always bugged me that I had to do stuff like this:
NSEnumerator *outerEnumerator = [array objectEnumerator]; NSDictionary *dictionary = nil; while ((dictionary = [outerEnumerator nextObject])) { NSEnumerator *innerEnumerator = [dictionary keyEnumerator]; id object = nil; while ((object = [innerEnumerator nextObject])) [self doSomethingWithObject:object]; }
I don't like having to worry about name clashes (outerEnumerator vs innerEnumerator). I don't like having to use three lines of code to set up the enumeration. I don't really care about the use of an assignment within a conditional expression (in the while statements) because I can tell GCC to warn me about places where it thinks I might have done it by accident, and I can explicitly suppress those warnings in places where I am sure that's what I want to do by using double parentheses as in the example above.
So Jonathan has his own preprocessor macros to address his concerns. Michael has a different macro. Here's my own take on the idea:
#define WOEnumerate(collection, object) \ for (id enumerator = [collection objectEnumerator], \ selector = (id)@selector(nextObject), \ method = (id)[enumerator methodForSelector:(SEL)selector], \ object = enumerator ? ((IMP)method)(enumerator, (SEL)selector) : nil; \ object != nil; \ object = ((IMP)method)(enumerator, (SEL)selector))
#define WOKeyEnumerate(collection, object) \ for (id enumerator = [collection keyEnumerator], \ selector = (id)@selector(nextObject), \ method = (id)[enumerator methodForSelector:(SEL)selector], \ object = enumerator ? ((IMP)method)(enumerator, (SEL)selector) : nil; \ object != nil; \ object = ((IMP)method)(enumerator, (SEL)selector))
These macros can be used like this in code:
WOEnumerate(fileArray, fileName) NSLog(@"Filename: %@", filename);
Things to note about the solution:
- Like Michael, I do throw away compile-time type checking (the object is always of type id) for the sake of simplicity.
- Like Jonathan, I make all of the variables local to the loop so that I don't have to worry about clashes.
- I provide two macros, one for object enumeration (for example, for use with NSArray collections) and one for key enumeration (for example, for use with NSDictionary collections). It would be trivial to add a third macro, WOReverseEnumerate, that would use the reverseObjectEnumerator selector.
- Like Michael, I cache both the method selector and implementation pointer. In informal testing (enumerating over a 10,000,000-item array ten times) the macro performed 49% faster than the standard idiom (averaging 3.6 million objects per second compared with 2.4 million per second). I tried other patterns but this one turned out to be the fastest.
- If passed a nil pointer instead of a valid collection no iterations at all are performed, matching the behaviour of the standard idiom.
- If passed an object which does not respond to the objectEnumerator selector (or keyEnumerator selector in the case of the WOKeyEnumerate macro) then an exception is raised, again matching the pattern of the standard idiom.
- Note that the compiler C dialect must be set to C99 or GNU99 in order to use this macro because of the declaration of variables inside the for statement.
- I have to use some casts between id, SEL and IMP types (in reality all just pointers) because C99 only allows a single declaration in the first expression of the for statement. In other words, "int i = 0, j = 0" (one declaration) is legal; "int i = 0, unsigned j = 0" (multiple declarations) is not. The casts are necessary to silence compiler warnings.
- If you pass a collection object that responds to the objectEnumerator (or keyEnumerator) selector, but which returns an enumerator that does not respond to the nextObject selector then bad things will happen (in which case you probably deserve to crash for having passed such a collection).
The one thing I don't like about this solution is that the macro looks like a function (name followed by parameters in parentheses) but behaves like a for statement (statement followed by expressions in parentheses followed by a block). That's probably while Michael named his macro foreach and swapped the order of the parameters (so that it reads like "for each item in collection". Nevertheless, I named my macros WOEnumerate and WOKeyEnumerate because I've made a habit of using the WO prefix to avoid namespace clashes, and I feel that the verb "enumerate" should appear in the macro.
Of course, I probably should have stuck to my conventions and used the form WO_ENUMERATE (I like to use all capitals for macros so that they scream out in the source code, "I am a macro and not a function!"; in general I make exceptions only where I want the macro to appear like an existing and closely-related Cocoa macro or function: for example, I have an WOStringFromBool macro that is intended to augment NSStringFromSelector and friends, and a WOAssert macro that's intended to supplement the NSAssert family). What was I thinking when I wrote these macros?... I suppose that suitable alternative names for the macros would be enumerate and keyenumerate; if used like that the macros would look like these when used:
enumerate (fileName, fileArray) NSLog(@"Filename: %@", filename);
So there you have it: three ways to improve upon the standard Objective-C enumeration idiom.
Update: 24 February 2006
Since my original post I've improved the macros in two ways:
- Firstly, I renamed the loop-local variables (enumerator, selector and method) to something more obscure so as to reduce the risk of namespace clashes.
- Later, used the GCC preprocessor macro concatentation operator (##) to further reduce the risk of namespace clashes by appending the value of object to those loop-local variable names. I use object because it has to be a simple scalar (unlike collection which could be something like "[thing message]").
The sum effect of these changes is that you should be able to use the macros pretty much any (including nesting them) without feature of variable scoping issues or namespace clashes. If you want to check out the finished macros, now named WO_ENUMERATE, WO_KEY_ENUMERATE and WO_REVERSE_ENUMERATE, see the WOEnumerate.h file in WOTest.
Posted by wincent at January 26, 2006 04:50 PM