Iterators in Cocoa

One thing I’ve always liked about C++ is that when using the Standard Template Library, you can write loops over containers (or really between any range of iterators) very cleanly and simply, e.g.:

<br /> for (vector<foo>::iterator i = myVector.begin(); i != myVector.end(); ++i) {<br /> // do something with *i<br /> }<br />

On the other hand, whenever I use Cocoa NSEnumerator objects, it drives me batty:

<br /> NSEnumerator *e = [myArray objectEnumerator];<br /> id object;<br /> while ((object = [e nextObject]) != nil) {<br /> // do something with object<br /> }<br />

Not only did it take me three lines instead of one to set up the loop, but I “leak” two variables out the other end. I’ve looked long and hard for a way to set up an NSEnumerater in one line in Objective-C++, but if it’s even possible, it almost certainly won’t fit into 80 columns.

I could use macros to make it simpler (as Michael Tsai alludes to), but I decided to try and re-work the Cocoa enumeration concept to act more like the operations on an STL Input Iterator. They’re the same concept, really, but the expressions are different.

The basic idea is to take NSEnumerator and separate nextObject into two methods. I wrote it up; you can download AKIterator.h and AKIterator.m. An AKIterator has an object method that returns the current object (and returns nil if it’s already at the end) and a next method to move to the next object. I even wrote categories on all the Cocoa classes that currently return NSEnumerator to return AKIterator instead, and a specialization for NSArray that’s a little more efficient in its use of memory and speed.

So now I can write:

<br /> for (AKIterator *i = [myArray objectIterator]; [i object]; [i next]) {<br /> // do something with [i object]<br /> }<br />

Neat, huh? Of course, now that I’ve gone through the exercise. I’ll probably put it in a drawer and never use it, because at the end of the day, NSEnumerator isn’t all that bad, and it’s not worth the effort of dragging this extra code around with me everywhere I go. But I got it out of my system.

7 thoughts on “Iterators in Cocoa

  1. I assume that you could set up an NSEnumerator in one line by putting the declarations in the first part of the “for” loop (and thus not “leaking” those variables). But, yeah, it would be wide. Is there a particular advantage to breaking -nextObject into -object and -next? (Java’s next() and hasNext() split makes some code cleaner, but I don’t, at the moment, see such advantages for the STL style.) Anyway, it’s a neat idea (categories are fun).

  2. I assume that you could set up an NSEnumerator in one line by putting the declarations in the first part of the “for” loop

    Not quite. The problem is that the two variables are dependent, but since the comma in a variable declaration statement doesn’t define a sequence point, the order of evaluation is undefined. So yes, this is syntactically-correct Objective-C++:

    for (id e = [myArray objectEnumerator], o = [e nextObject]; o != nil; o = [e nextObject])
    

    But what happens if the compiler decides to evaluate [e nextObject] before e is initialized? Bad Things. Also, you have to write [e nextObject] twice, and that makes nobody happy :)

    (By the way, you do have to define e as id and not NSEnumerator*, since you can only declare variables of a single type in a given statement.)

    I guess you could write this, if you really wanted to do it. But I wouldn’t recommend it:

    for (id o, e, dummy = (id)((e = [myArray objectEnumerator]) && (o = [e nextObject])); o != nil; o = [e nextObject])
    

    Anyway, the reason for having a separate object method in AKIterator, besides it being like the STL (it makes more obvious sense in C++, where you’ve got operator overloading and can make cool use of the -> operator), is that it means you have an idempotent operation to retrieve the object. With Cocoa or Java, I have one and only one chance to get an object, and if I’m going to need it more than once, I had better make a copy. With AKIterator, I can call [i object] as many times as I want. And since in Cocoa (unlike Java), nil is not a valid value in a container, I can use a nil result from object to signify the end of the list, so I don’t have to have a separate hasNext method.

  3. The problem is that the two variables are dependent, but since the comma in a variable declaration statement doesn’t define a sequence point, the order of evaluation is undefined.

    Interesting…do you have a reference for your point about the comma not defining a sequence point? Page 123 of _The C++ Programming Language (3rd Ed.) says “The operators , (comma), && (logical and), and || (logical or) guarantee that their left-hand operand is evaluated before their right-hand operand.” Maybe the comma in a variable declaration statement is not actually the comma operator. I didn’t see anything in Stroustrup’s book that made this clear, but I did find this quote from C/C++ User’s Journal: “If the same declaration declares multiple initialized objects, the ‘assignments’ occur in the order the objects are declared.”

    It means you have an idempotent operation to retrieve the object.

    Sorry, I’m feeling thick here. I see that [i object] is idempotent, but how is that better than having a loop-scope variable o for the object? [i object] isn’t making a copy; it just returns the same object each time. So in each case you have an expression that you can evaluate any number of times to get the same object.

    With hasNext in Java, I can tell, from inside the loop body, whether I’m on the last iteration. I don’t see how to do that with NSEnumerator or AKIterator. With both of those, I have to actually call nextObject/next (which are not idempotent) before I can check for the nil value. If I get something non-nil, I can’t “put it back” to continue the loop as normal. Basically, what I’m saying is that hasNext lets you “peek,” whereas NSEnumerator and AKIterator only let you “pop.”

  4. Interesting…do you have a reference for your point about the comma not defining a sequence point?

    K&R, page 63: “The commas that separate function arguments, variables in declarations, etc., are not comma operators, and do not guarantee left to right evaluation.” Of course, C++ may be different. I don’t have a copy of the C++ standard to check.

    With hasNext in Java, I can tell, from inside the loop body, whether I’m on the last iteration. I don’t see how to do that with NSEnumerator or AKIterator.

    Excellent point. Of course, the real problem is that NSEnumerator (and hence AKIterator) is a model of what the STL calls a Input Iterator. Cocoa really needs a more flexible sequence class. For example, all the STL container-generated iterators are models of a Forward Iterator or a refinement thereof. Given a Forward Iterator, I can very easily check to see if I’m at the last element.

    For example, here’s a C++ template function that prints out the elements of a range, seperated by commas:

    template <class ForwardIterator>
    void print(ForwardIterator begin, ForwardIterator end) {
        for (ForwardIterator i = begin; i != end; ++i) {
            cout << *i;
    
            ForwardIterator j = i;
            if (++j != end) cout <&lt ", ";
        }
    }
    

    Of course, if you’ve got a Random Access Iterator, you can just write i + 1 != end. Random Access Iterators can do plenty of things even Java can’t do.

  5. Page 223 of K&R says that “initialization…proceeds in the order of
    the declarators.” Page 215 says that a “declarator” is the variable
    name component (“foo” or “*bar” or “baz[20]”), not the declaration
    itself (“int foo”).

  6. I took the full step and created iterators for several Cocoa collections (NSArray, NSDictionary, NSSet, NSIndexSet & NSString).

    The array iterator is random access and assignable, so it’s e.g. possible to write:

       NSMutableArray* array = ...;
       std::random_shuffle(beginof(array), endof(array));
    

    Here beginof and endof are helper functions to return the first/last iterator of the sequence, there are also overloads for STL containers and primitive arrays, to keep the syntax consistent.

    I also created insert iterators for some of the collections, allowing code like:

       NSMutableArray* array = [NSMutableArray array];
       id src[] = { @"foo", @"bar", @"fud" };
       std::copy(beginof(src), endof(src), back_inserter(array));
    

    And there is also a helper function to adapt a selector to a functor, so to convert an NSArray of NSNumbers to a std::vector<int>, use the following:

       NSArray* array = ...;
       std::vector<int> v;
       std::transform(beginof(array), endof(array), back_inserter(v)
          method<int>(@selector(intValue))
       );
    

    More info here

  7. Pingback: Michael Tsai's Weblog

Comments are closed.