views:

56

answers:

1

I'm running into an odd issue in Objective-C when I have two classes using initializers of the same name, but differently-typed arguments. For example, let's say I create classes A and B:

A.h:

#import <Cocoa/Cocoa.h>

@interface A : NSObject {
}

- (id)initWithNum:(float)theNum;

@end

A.m:

#import "A.h"

@implementation A

- (id)initWithNum:(float)theNum
{
    self = [super init];
    if (self != nil) {
        NSLog(@"A: %f", theNum);
    }
    return self;
}

@end

B.h:

#import <Cocoa/Cocoa.h>

@interface B : NSObject { 
}

- (id)initWithNum:(int)theNum;

@end

B.m:

#import "B.h"

@implementation B

- (id)initWithNum:(int)theNum
{
    self = [super init];
    if (self != nil) {
        NSLog(@"B: %d", theNum);
    }
    return self;
}

@end

main.m:

#import <Foundation/Foundation.h>

#import "A.h"
#import "B.h"

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    A *a = [[A alloc] initWithNum:20.0f];   
    B *b = [[B alloc] initWithNum:10];

    [a release];
    [b release];

    [pool drain];
    return 0;
}

When I run this, I get the following output:

2010-04-26 20:44:06.820 FnTest[14617:a0f] A: 20.000000
2010-04-26 20:44:06.823 FnTest[14617:a0f] B: 1

If I reverse the order of the imports so it imports B.h first, I get:

2010-04-26 20:45:03.034 FnTest[14635:a0f] A: 0.000000
2010-04-26 20:45:03.038 FnTest[14635:a0f] B: 10

For some reason, it seems like it's using the data type defined in whichever @interface gets included first for both classes. I did some stepping through the debugger and found that the isa pointer for both a and b objects ends up the same. I also found out that if I no longer make the alloc and init calls inline, both initializations seem to work properly, e.g.:

A *a = [A alloc];
[a initWithNum:20.0f];

If I use this convention when I create both a and b, I get the right output and the isa pointers seem to be different for each object.

Am I doing something wrong? I would have thought multiple classes could have the same initializer names, but perhaps that is not the case.

+5  A: 

The problem is that the +alloc method returns an object of type id so the compiler can't decide which method signature to use. You can force your application to choose the correct selector in a number of ways. One would be to cast the return from alloc, so:

A* a = [(A*)[A alloc] initWithNum:20.f];
B* b = [(B*)[B alloc] initWithNum:10];

Or you could override alloc on your class and return something more specific, although I wouldn't do this myself. So:

+ (A*)alloc { return [super alloc]; }

Finally, and what I would personally chose, make the selectors more descriptive:

// A.h
- (id)initWithFloat:(float)theNum;

// B.h
- (id)initWithInteger:(int)theNum;
Jason Coco
You can also add static `typeWithArgument:` methods, like `[NSNumber numberWithInt:]` to disambiguate.
drawnonward
According to the documentation, the `isa` ivar should be set when you call `+[NSObject alloc]`. Since Objective-C message dispatches are resolved at runtime, isn't it the runtime's job, not the compiler's job, to work out what `+[A alloc]` resolves to?
Nick Forge
@Nick Forge: The implementation is dynamically bound so that is not a problem (and that holds true here since the NSLog shows A and B as expected). Where the compiler is involved is in building the call to obj_msgSend. In this case, one selector uses a float while the other uses an integer, and the compiler generates different instructions (storing the int in an integer register and the float in a fp register). Because of this, the implementation of the method finds bad data whne it looks in the expected register.
Jason Coco
@drawnonward: I don't see how that's any different from the last bit that I said I would use (i.e., initWithFloat:). Or do you mean using an actual NSNumber instead of scalar values in the init method (e.g.,initWithNumber:(NSNumber*)theNum)?
Jason Coco
@Jason Coco that makes sense. Shouldn't your answer make this somewhat clearer? You say "so the compiler can't decide which selector to call", when it's really the argument types that is the issue, not the selector, since the selector/IMP resolution happens at runtime, and it's an `int`/`float` argument mismatch that causes the error. Given that, using initialisers like `initWithFloat:` shouldn't cause a problem, since every (sane) `initWithFloat:` initialiser will take a `float` as an argument.
Nick Forge
@Nick Forge: I guess it is somewhat gray since it is getting the method signature from the selector although it is not the dynamic runtime binding that is causing the issue. And I agree, my preferred way would be to make the selector itself more descriptive and sane, as you pointed out ;) but just casting so that the compiler can choose the correct method signature will work as well. I should probably change the word "selector" in my answer to method signature, but I am lazy so feel free to edit away :)
Jason Coco
I don't have enough SO rep to edit, sorry. :-)
Nick Forge
@Nick Forge: Ok, done :)
Jason Coco
@Jason Coco: making a static wrapper that calls init for you includes both the argument type and result type in the name, and can hide any typecasting you need for alloc in a convenient wrapper. If you want to use initWithCommonNoun: you can make classOneWithCommonNoun: and classTwoWithCommonNoun: even if the type for commonNoun is different in each case, as with Num in the original example. I prefer to use the purpose of an argument not the type, but you could still end up with name overlaps.
drawnonward
@drawnonward: Slight pedantic point: there are no static methods in Objective-C, what you suggest should properly be referred to as a class method. Anyway, I wouldn't hesitate to use your pattern in most cases, but where performance and memory are important, alloc - init may be better because you need to autorelease the created object in your class method and (possibly) retain it in the method that asks for the object.
JeremyP