tags:

views:

104

answers:

5

I've tried to use id to create duck typing in objective-c. The concept looks fine in theory but failed in practice. I was unable to use any parameters in my methods. The methods were called but parameters were wrong. I was getting BAD_ACESS for objects and random values for primitives. I've attached a simple example below.

The question: Does any one knows why the methods parameters are wrong? What is happening under the hood of the objective-c?

Note: I'm interest in the details. I know how to make the example below work.

An example: I've created a simple class Test that is passed to an other class using property id test.

@implementation Test
- (void) aSampleMethodWithFloat:(float) f andInt: (int) i {
    NSLog(@"Parameters: %f, %i\n", f, i);
}
@end

Then in the class the following loop is executed:

for (int i=0; i < 10; ++i) {
    float f=i*0.1f;
    [tst aSampleMethodWithFloat:f andInt:i]; // warning no method found.
}

Here is the output that I'm getting. As you can see the method was called but the parameters were wrong.

Parameters: 0.000000, 0
Parameters: -0.000000, 1069128089
Parameters: -0.000000, 1070176665
Parameters: 2.000000, 1070805811
Parameters: -0.000000, 1071225241
Parameters: 0.000000, 1071644672
Parameters: 2.000000, 1071854387
Parameters: 36893488147419103232.000000, 1072064102
Parameters: -0.000000, 1072273817
Parameters: -36893488147419103232.000000, 1072483532

Update:

I've found out by accident that when I add a declaration of aSampleMethodWith... to the class with for loop the warning disappears and the method on the Test class is called correctly.

Update 2: As pointed out by JeremyP the direct cause of the problem is that the floats are treated as doubles. But anyone knows why? (following the 5why principle :) ).

According to @eman the call is translated to simple C function call and compiler directive to get the SEL. So the @selector gets confused. But why? The compiler have all necessary type informations in the first method call. Does any one knows a good source of information about the Objective-C internals I've search The Objective-C Programming Language but i didn't find the answer.

A: 

You can make the warning go away by making a category like this:

@interface NSObject (MyTestCategory)
- (void) aSampleMethodWithFloat:(float) f andInt: (int) i;
@end
neoneye
Good point but why objective-c get confused? Why I'm getting warning instead of compilation error?
Piotr Czapla
A: 

Without a signature available at the calling point, it isn't known what type the parameters are supposed to have. Undefined methods will be assumed to take ... as parameters, which isn't what yours does. If there is any interface seen by the compiler at this point, where the method in question exists, that definition will be used.

calmh
Wow. I was expecting ints, obejcts, but not ... :). Thank you.
Piotr Czapla
Do you know where I can find such details about objective c? Any good/advance source of information?
Piotr Czapla
Are you sure about ... ? Look at @JeremyP answer. After I've changed the float to double the code started to work correctly.
Piotr Czapla
+2  A: 

By default floating point values are passed as doubles, not floats. The compiler does not know, at the point where [tst aSampleMethodWithFloat:f andInt:i]; occurs that it is only supposed to pass a float, so it promotes f to a double. This means that, in the method, when the compiler does know it is dealing with a float, f is the float formed by the first four bytes of the double passed to the method and i is an int formed from the second four bytes of the double passed.

You can fix this by either

  • changing the first parameter of aSampleMethodWithFloat:andInt: to a double
  • importing the interface declaration of Test into the file where you use it.

NB there is no gain except a small amount of space when using floats in C. You might as well use doubles everywhere.

JeremyP
I'm programing for iphone where there is no hardware doubles so i use floats everywhere. But let me try how does your solution work.
Piotr Czapla
It works indeed. It seems that the behavior is inherited from C, I didn't know about float to double conversions.
Piotr Czapla
*NB there is no gain except a small amount of space when using floats in C.* — Tell it to Apple. They use CGFloat (which is `float`) everywhere :\ .
KennyTM
@JeremyP Do you know why floats get converted to doubles? If we look at the method call as a call to an regular C function `objc_msgSend` as described by @eman? Where is the conversion?
Piotr Czapla
@Piotr Czapla: I think it has something to do with functions without prototypes. `float` is promoted to `double` when passed to a function without a known argument size. See C99: 6.5.2.2.6.
dreamlax
@Piotr: it's a standard C thing like dreamlax says. The same occurs with unknown integer types - int is assumed by default..
JeremyP
@KennyTM: I can only assume that there were so many uses of CGFloat in the graphics subsystem that Apple decided the space saving overruled the slower arithmetic (on machines with floating point coprocessors). NB CGFloat is a double in the OS X 64 bit runtime.
JeremyP
+1  A: 

I think JeremyP is correct about the problem being about doubles vs floats. As for implementation details, message dispatch in Objective-C uses the objc_msgSend(id theReceiver, SEL theSelector, ..) C function (for some deep nitty-gritty, see here). You can simulate the same results of method dispatch like so:

SEL theSelector = @selector(aSampleMethodWithFloat:andInt:);
objc_msgSend(self.test, theSelector, 1.5f, 5);

SEL is just a number that corresponds to a function (that is dynamically determined based on the method signature). objc_msgSend then looks up the actual function pointer (of type IMP) of the method and invokes it. Since objc_msgSend has a variable number of arguments, it will just use as many as you pass in. If you were to do:

objc_msgSend(self.test, theSelector, 1.5f);

It would use 1.5f correctly and have junk for the other variable. Since the method signature typically denotes the number of arguments, this is hard to do under normal usage.

eman
I've mixed two version of my code. The method name should be aSampleMethodWithFloat:andInt: in both places. See the updated question.
Piotr Czapla
@Piotr Czapla: OK, updated the answer.
eman
But Why It gets confused when the method definition is not available? Why it treat floats as doubles? I guess it is connected with the way that C treat unknown functions but i'm having hard time to understand how it is connected.
Piotr Czapla
Someone with more C knowledge than me will have to answer. The overall answer to your question, though, is that it's possible to use duck typing with Objective-C, but most certainly not with straight C (although I'd recommend against using excessive duck-typing in Objective-C--the (keyboard) typing saved isn't worth the lack of typesafety). If you were to have used `NSNumber`, for instance, there wouldn't have been a problem. C is extremely un-typesafe--always tread with caution when dealing with C types.
eman
A: 

The trouble here is with the dividing line between C and Objective-C. The id type specifies any object, but ints and floats are not objects. The compiler needs to know the C type of all the arguments and the return type of any method you call. Without a declaration, it assumes that a method returns id and takes an arbitrary number of id arguments. But id is incompatible with int and float, so the value doesn't get passed correctly. That's why it works correctly when you provide a declaration — then it knows your int is an int and your float is a float.

Chuck
The floats are getting converted to doubles. So it is less likely that they are treaded as id. The example works if I change the method to accept double and int. Note that I'm still passing a float to the function.
Piotr Czapla