Monsters from the id

Bolot Kerimbaev's Headshot
Bolot Kerimbaev cocoa ios

Sometimes things aren't as simple as they appear. One of my former students was asking what id * meant. This seemed straightforward enough, but as I started exploring the question in more detail, things were becoming curiouser and curiouser.

At first blush, the answer is simple. id means a pointer to an Objective-C object, thus id * must mean a pointer to a pointer to an Objective-C object. For example, if we have a variable id someVariable, then the expression &someVariable would be of type id *.

Here's a more familiar example, using NSError *, instead of id:

NSError *error;
    NSString *names = [NSString stringWithContentsOfFile:@"/usr/share/dict/propernames"
                                                encoding:NSUTF8StringEncoding
                                                   error:&error];

We declare a local variable error that's a pointer to an instance of NSError. The third parameter of stringWithContentsOfFile:encoding:error: is NSError**. The ampersand gives the address of the error variable, which is a pointer to NSError. error is of type NSError *, &error is of type NSError **. Side note: with ARC, we don't have to explicitly initialize stack variables to nil, the compiler takes care of that for us.

Let's make things a bit more interesting by writing our own function that will take a pointer to a pointer to an Objective-C object. This function will ensure that our object isn't nil, populating it with some default value as needed:

BOOL ensureNotNil(id * ptr)
    {
        id obj = *ptr;
        if (!obj) {
            *ptr = @"some-default-value";
            return YES;
        }
        return NO;
    }

It's a pretty straight-forward function: first, we dereference ptr to get an object reference. Then, we check if the object reference is nil and if so, we modify the pointer that ptr points to, to reference a literal NSString instance.

Here's how we would use it:

id someObject;
    BOOL result = ensureNotNil(&someObject);
    NSLog(@"someObject is %@, result was %@", someObject, result ? @"YES" : @"NO");
    someObject = @"this is MARTA";
    result = ensureNotNil(&someObject);
    NSLog(@"someObject is %@, result was %@", someObject, result ? @"YES" : @"NO");

The output is what we expect it to be:

someObject is some-default-value, result was YES
    someObject is this is MARTA, result was NO

Note that the compiler automatically inserted the __autoreleasing qualifier for ensureNotNil function's parameter. If you start typing it in code, the code completion will suggest the following:

ensureNotNil(__autoreleasing id *ptr)

Now a bit curiouser: Let's say we have a Person class with an NSString * property called name. And we want to ensure that the name isn't nil. Could we do this?

@interface Person : NSObject
    @property NSString *name;
    @end
    
    @implementation Person
    - (BOOL)ensureNameNotNil
    {
        return ensureNotNil(&self.name);
    }
    @end

Turns out, no, the compiler will give us an error: "Address of property expression requested". The dot notation is equivalent to sending the message to self:

- (BOOL)ensureNameNotNil
    {
        return ensureNotNil(&[self name]);
    }

If we rewrite the expression to reference the backing variable, we get a different error, "Passing address of non-local object to __autoreleasing parameter for write-back":

- (BOOL)ensureNameNotNil
    {
        return ensureNotNil(&_name);
    }

Now, this error message makes a bit more sense! Autoreleased lifetime is typically used for local variables, so that they can be flushed when the autorelease pool is drained. But it would make no sense to use autorelease with instance variables. Thus, to fix the example, we have to write it like this:

- (BOOL)ensureNameNotNil
    {
        NSString *aName = [self name];
        BOOL result = ensureNotNil(&aName);
        [self setName:aName];
        return result;
    }

The compiler might rewrite this method like this:

- (BOOL)ensureNameNotNil
    {
        NSString * __strong aName = [self name];
        NSString * __autoreleasing tmp = aName;
        BOOL result = ensureNotNil(&tmp);
        aName = tmp;
        [self setName:aName];
        return result;
    }

What we've learned so far, parameters of type NSError ** or id * have to be references to local variables. Is it possible to create a local variable that is of type NSError ** or id *? If we write something like this:

id someObject = @[@"alpha"];
    id * pointerToSomeObject = &someObject;

we get an error: "Pointer to non-const type 'id' with no explicit ownership". This is ARC's way of saying, "I have no idea what the lifetime of the variable would need to be". If we wrote the same code in a non-ARC project, we would not get this error. Using the error message as a hint, we can try to fix it as follows:

id someObject = @[@"alpha"];
    id const * pointerToSomeObject = &someObject;

This means that "pointerToSomeObject is a pointer to a constant pointer to an Objective-C object". Unfortunately, it's not a particularly meaningful construct. It's still possible to modify pointerToSomeObject and cause some trouble:

pointerToSomeObject += 42;
    NSLog(@"This is gonna cause a crash %d", [*pointerToSomeObject count]);

But going back to basics, do we really know what id is? Isn't id the same thing as NSObject *? Not quite. Let's explore some differences:

id firstObject = @[@"alpha"];
    NSObject *secondObject = @[@"beta"];

So far, fairly similar: both can be assigned instances of NSArray. However, if we try to send the messages, we get a different picture:

NSUInteger firstCount = [firstObject count];
    NSUInteger secondCount = [secondObject count];

The first line is just fine, the compiler won't complain. But the second one will error out: "No visible @interface for 'NSObject' declares the selector 'count'". This makes sense, since count is declared in NSArray, but not in NSObject. But why isn't an error shown for firstObject? That's because id is treated specially and doesn't do type checking. Of course, whether or not the message is understood at run time is a different story. That's why id is frequently used together with the protocol(s) that the object must implement:

id < NSURLConnectionDelegate > delegate;

Curiously, doing this limits the code completion suggestions to the ones understood by NSURLConnectionDelegate and no longer allows sending the count selector to this object.

So, what is id? In objc/objc.h, we find id defined as follows:

typedef struct objc_object {
        Class isa;
    } *id;

This means that id is defined as a pointer to the struct objc_object. It's not defined in terms of NSObject. What is NSObject and aren't all classes subclasses of NSObject? Turns out, no. First, looking in NSObject.h, we discover that NSObject is a… protocol, among other things. NSObject is also the name of the class we all know and love. And it implements NSObject, the protocol:

NS_ROOT_CLASS
    @interface NSObject <nsobject> {
        Class isa;
    }

We could create our own class completely outside of the NSObject hierarchy, but the first problem we'd run into is how to create instances, since alloc is a class method in NSObject class. Maybe we'll revisit this idea next time. But for now, there are classes that don't inherit from NSObject, such as NSProxy:

NS_ROOT_CLASS
    @interface NSProxy < NSObject > {
        Class isa;
    }

You can read more about ARC, __autoreleasing, and write back parameters:

Recent Comments

comments powered by Disqus