NSFastEnumeration object casting in ARC

657 views Asked by At

I'm trying to implement the countByEnumeratingWithState:objects:count: method from the NSFastEnumeration protocol on a custom class.

So far I have it iterating through my objects correctly, but the objects that are returned aren't Objective-C objects but rather the core foundation equivalents.

Here's the part of the code that sets the state->itemsPtr:

MyCustomCollection.m

- (NSUInteger) countByEnumeratingWithState: (NSFastEnumerationState *)state
                                   objects: (id __unsafe_unretained *)buffer
                                     count: (NSUInteger)bufferSize {

    // ... skip details ...

    NSLog(@"Object inside method: %@", someObject);
    state->itemsPtr = (__unsafe_unretained id *)(__bridge void *)someObject;      

    // ... skip details ...
}

Then I call the 'for..in' loop somewhere else on like this

SomeOtherClass.m

MyCustomCollection *myCustomCollection = [MyCustomCollection new];
[myCustomCollection addObject:@"foo"];
for (id object in myCustomCollection) {
    NSLog(@"Object in loop: %@", object);
}

The console output is:

Object inside method: foo
Object in loop: __NSCFConstantString

As you can see, inside the NSFastEnumeration protocol method the object prints fine, but as soon as it gets cast to id __unsafe_unretained * I lose the original Objective-C corresponding class.

To be honest I'm not quite sure how the (__unsafe_unretained id *)(__bridge void *) casting works in this case. The (__unsafe_unretained id *) seems to cast to match the right type itemsPtr needs. The (__bridge void *) seems to cast to a pointer of type void with __bridge used to bridge the obj-c world to the CF world. As per the llvm docs, for __bridge:

There is no transfer of ownership, and ARC inserts no retain operations

Is that correct?

From my understanding __NSCFConstantString is just the core foundation equivalent of NSString. I also understand that with ARC you need to bridge from Objective-C objects to CoreFoundation equivalents because ARC doesn't know how to manage the memory of the latter.

How can I get this working so that the objects in my 'for..in' loop are of the original type?

Also note that in this case I'm adding NSStrings to my collection but in theory it should support any object.

UPDATE

Rob's answer is on the right track, but to test that theory I changed the for loop to this:

for (id object in myCustomCollection) {
    NSString *stringObject = (NSString *)object;
    NSLog(@"String %@ length: %d", stringObject, [stringObject length]);
}

In theory that should work since the objects are equivalent but it crashes with this error:

+[__NSCFConstantString length]: unrecognized selector sent to class

It almost looks like the objects returned in the for loop are classes and not instances. Something else might be wrong here... Any thoughts on this?

UPDATE 2 : SOLUTION

It's as simple as this: (thanks to CodaFi

state->itemsPtr = &someObject;
2

There are 2 answers

5
CodaFi On BEST ANSWER

You're incorrectly casting someObject. What you meant is:

state->itemsPtr = (__unsafe_unretained id *)(__bridge void *)&someObject;

(Let's get rid of those awful casts as well)

state->itemsPtr = &someObject;

Without the address-of, your variable is shoved into the first pointer, which is dereferenced in the loop. When it's dereferenced (basically, *id), you get the underlying objc_object's isa class pointer rather than an object. That's why the debugger prints the string's value inside the enumerator call, and the class of the object inside the loop, and why sending a message to the resulting pointer throws an exception.

4
rob mayoff On

Your code is fine the way it is. Your debug output is revealing an implementation detail.

NSString is toll-free-bridged with CFString. This means that you can treat any NSString as a CFString, or vice versa, simply by casting the pointer to the other type.

In fact, under the hood, compile-time constant strings are instances of the type __NSCFConstantString, which is what you're seeing.

If you put @"hello" in your source code, the compiler treats it as a NSString * and compiles it into an instance of __NSCFConstantString.

If you put CFSTR("hello") in your source code, the compiler treats it as a CFStringRef and compiles it into an instance of __NSCFConstantString.

At run-time, there is no difference between these objects in memory, even though you used different syntax to create them in your source code.