2

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;
nebs
  • 4,939
  • 9
  • 41
  • 70

2 Answers2

3

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.

CodaFi
  • 43,043
  • 8
  • 107
  • 153
  • You're right! Silly mistake on my part. Looks like I have to review my C pointer syntax.. Like you said, it's a bit of a fluke that this worked out the way it did given the underlying struct representation of objects. – nebs Jun 15 '13 at 06:30
  • 1
    With the `&` you can get rid of both of those awful casts as well. – CodaFi Jun 15 '13 at 06:32
  • Brilliant! Could you explain briefly why this works? itemsPtr is defined like this in the struct: `id __unsafe_unretained *itemsPtr`. From my understanding `id` means 'pointer to something'. So if we ignore the `__unsafe_unretained` part we have `id *` as the type, which means `pointer to a pointer to something` correct? Then `&` means 'address of'. So when I do &someObject it's giving me back an integer that is the memory address of the object. I'm not sure I understand why &myObject can be assigned to `id *`. Does itemsPtr point to another pointer which now holds myObject's address? – nebs Jun 15 '13 at 06:39
  • (sorry for the long question, I should probably just go review C pointers) – nebs Jun 15 '13 at 06:40
  • id is not a pointer to something, it's a pointer to `objc_object`. id* is a double pointer to `objc_object`. You need the & because a pointer to a class is just a single pointer to `objc_object`. It's why you have to write `NSString *var`, but only `id var`. When you dereference id, you get the value that is first in the layout of the struct it points to, which is `Class isa` – CodaFi Jun 15 '13 at 06:40
1

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Thanks for your answer, that makes perfect sense, and that was roughly what I thought was going on. See the update to my question though, I tested your theory with an explicit cast but I can't seem to call NSString methods on the __NSCFConstantString object. Do I need to do something special to cast into an obj-c object? – nebs Jun 15 '13 at 06:17
  • @Nebs No, casting doesn't do **anything at all** (apart from fooling the compiler, of course). It won't change the class of the object nor the messages it responds to. `__NSCFConstantString` is, however, a proper subclass of `NSString`, so it **must** respond to all the messages `NSString` responds to. Why you are getting the error is that you are trying to send it to **the class object itself** and not one an instance of it. –  Jun 15 '13 at 06:27
  • Yeah you're right. I was passing the wrong thing to the itemsPtr, so my problem was there rather than with casting. While your answer is technically write, CodaFi's answer got to the root of this question so I will accept his answer. Thanks for the clarification! – nebs Jun 15 '13 at 06:30
  • 1
    It's because if the way `objc_object`'s laid out in memory that this doesn't fail more catastrophically than it already does. I think I've explained it in my answer – CodaFi Jun 15 '13 at 06:31