October 9, 2005

Pimp, Pimp Thyself.

An anonymous comment on my last post got me thinking about my favorite macro (which I'd posted), and how it could be improved.

Here it is again, for reference:
DMCommonMacros.h
static inline BOOL IsEmpty(id thing) {
return thing == nil
|| ([thing respondsToSelector:@selector(length)]
&& [(NSData *)thing length] == 0)
|| ([thing respondsToSelector:@selector(count)]
&& [(NSArray *)thing count] == 0);
}

Now, I like this macro because I don't have to worry about the class of the Cocoa object I pass into it; it pretty much works for any primitive type that has an 'empty' state. (Notably not NSScanner, though.)

Some people didn't like it because it does an extra compare with nil, which is unnecessary under old versions of Cocoa because sending any method to nil returns nil, which is synonymous with the integer 0 or the value FALSE on all current implementations of Cocoa (but NOT synonymous with the float 0.0, DO NOT BE FOOLED).

However, Apple is attempting to deprecate this shortcut, because they can make the runtime more efficient if they know that no receivers are nil (c.f. the new GCC 4.0 compiler flags: GCC_NO_NIL_RECEIVERS or -fno-nil-receivers).

Since a nil comparison is REALLY quick: like, you can do a billion a second, since a zero comparison should be one instruction for a in-register variable, and you're going to have to load the variable up for the Objective-C call to -length or -count if you're not nil anyways. Also, if the receiver *is* nil, you never have to send the obj-c message at all, which saves you a function call, so this is actually faster.

So, I think the explicit comparison with nil is great. But, what about the -respondsToSelector:? It struck me that GNU C has had (for a LONG time) the ability to tell the type of a variable passed into a macro. Now, I'm not really good with this stuff (I've only used it once or twice), so I don't know if this is possible, but it seems like it MIGHT be possible to rewrite the macro so that if 'thing' is of a known type (NSArray or NSString or whatever) then we immediately check it for nil and then send it the CORRECT method (-count or -length or whatever, respectively), and we only fall back on the code I have if we pass in a variable that is truly of type 'id', which is pretty rare.

Possible? I'm not sure. But there's a handmade, one-of-a-kind Delicious Monster stuffed sock monster for the first person who does it and posts the (non-copyrighted) code here for everyone to use. (You can see an example of this artist's other work on the DrunkenBlog, where drunkenbatman has snaps of the cow she did for his logo.) Unlike the 'virus' post a couple days ago, this is not a trial balloon; contest starts RIGHT NOW and you get the sock monster if your code works, biggity-bam.

Labels:

52 Comments:

Anonymous Jonathan Johnson said...

There isn't any way in C (nor Objective-C). Perhaps I can get the prize by explaining why :)

The C Preprocessor doesn't know any type information. It only deals with several statements, such as #define, #if, #ifdef, etc, and the expansion of defines.

The operators such as typeof() and sizeof() are evaluated later in the compilation process, by the actual compiler itself.

Even using the typeof() operator isn't possible, as there is no way to compare the "return" value of the intrinsic function. Typeof can be used for defining a variable inside of a macro, but not for much else.

Also, if you could somehow compare the type of a variable with typeof(), it wouldn't handle the cases where you subclass NSArray, NSMutableArray, or so forth. It would be constricted to the hard-coded checks used.

You might yield better performance if you use the isa: responder instead, but I bet the differences in speed would be negligible in most uses.

October 09, 2005 3:43 PM

 
Anonymous Jonathan Johnson said...

*clears throat* I could have sworn there was an isa method on NSObject. However, it appears I was mistaken. I meant isSubclassOf:.

October 09, 2005 3:46 PM

 
Anonymous Francisco Tolmasky said...

Well I can't do it use it macros, btu there is a trick to get around it if you're willing to compile that one file as Objective-C++ instead of just Objective-C.

Using C++ we can define multiple versions of the same function but with different parameters, so we would just do the following:

static inline BOOL isEmpty(NSArray *array)
{
return !array || ![array count];
}

static inline BOOL isEmpty(NSString *string)
{
return !string || ![string length];
}

static inline BOOL isEmpty(id thing)
{
//... catch all code you posted above.
}

This way, when the compiler comes to a isEmpty(@""), itll use the NSString specific function and the class checkign will be done at compile time instead of runtime. However, we keep the (id thing) one for a case when the class is not known till runtime, or in case there is a class that supports count or length but that we forgot to write a specific function for.

Again though, you'll have to change the name of your file to .mm from .m to make sure it uses C++, because this doesn't fly in the Objective-C world.

Hope this helps!

October 09, 2005 4:13 PM

 
Blogger harveyswik said...

Object type is a dynamic issue and so I believe not something the compiler can solve for you.

Here's an example of using two macros as suggested earlier:

static inline BOOL hasNoLength(id thing) {
return thing == nil || ([thing length] == 0);
}

static inline BOOL hasNoCount(id thing) {
return thing == nil || ([thing count] == 0);
}

And here's an example of using a category on NSObject with a single macro to achieve the same thing:

@interface NSObject (myCount)
- (unsigned)count;
@end

@implementation NSObject (myCount)
- (unsigned)count
{
return 1;
}
@end

@implementation NSString (myCount)
- (unsigned)count
{
return [self length];
}
@end

@implementation NSData (myCount)
- (unsigned)count
{
return [self length];
}
@end

@implementation NSIndexPath (myCount)
- (unsigned)count
{
return [self length];
}
@end

@implementation NSStatusItem (myCount)
- (unsigned)count
{
return [self length];
}
@end


static inline BOOL isEmpty(NSObject * thing)
{
return thing == nil || [thing count];
}

October 09, 2005 4:22 PM

 
Blogger Lucas Eckels said...

First of all, you're not using a macro, you're using a static inline function. This means the preprocessor doesn't really come into play (at least for how you're using it).

Secondly, runtime type identification will require an O(n) operation, where n is the height of the inheritance hierarchy down to the current object's class. respondsToSelector: should be a contant time operation, so is a better choice.

What I would do to solve this problem is to define some categories which have an isEmpty: method (or, since I know you hate categories that can collide with Apple, DMisEmpty: or something). Put one on NSObject which has a body like your inline function. Put one on NSString which just tests length: against 0, and one of NSArray which tests count: against 0.

Advantages:
1) Uses consistent calling convention -- IsEmpty(obj) doesn't look like ObjC to me, it looks like C which tries to fake objects with structs. [obj isEmpty] is much nicer.
2) You get the right call on the right object, as desired.
3) You still get a catch-all for any objects you've missed.

Disadvantages:
1) ObjC messages are slower than inlined C code.
2) More code to maintain.
3) You already have a pile of code which uses the existing IsEmpty() function.

October 09, 2005 4:36 PM

 
Blogger Wil Shipley said...

Lucas: The problem with calling -isEmpty is if the object were 'nil', it'd return -isEmpty would return 'nil', or FALSE, eg, "I am not empty."

Jonathan: The question is, can one use typeof() in an #if statement, assuming the IsEmpty() inline became a macro?

October 09, 2005 5:14 PM

 
Blogger Rory Prior said...

You can use [someObject class] == [KnownObject class] to determin whether two objects are of the same type, I use this in various places in my own code where I need to pass in an object where it may be one of several types. Of course you need to check whether the object is nil or not before you attempt to discern its type.

NSObject also has isKindOfClass: which saves you doing the == bit if you prefer.

October 09, 2005 5:22 PM

 
Blogger harveyswik said...

isKindOfClass does more than that

NSArray * anArray = [NSArray withObject:@"aString"];
if([anArray class] == [NSArray class]){
//code that never sees the light of day
}

if([anArray isKindOfClass:[NSArray class]]){
//code that gets executed
}

October 09, 2005 5:38 PM

 
Blogger Wil Shipley said...

Rory: You almost never want to use == on classes, as Harvey points out.

However, since -isKindOfClass: is going to be about the same as -respondsToSelector:, it's not really germane to making the IsEmpty() function better. We need a way to tell at compile time what kind of class we have so we emit only the minimal code needed to tell if it's empty.

October 09, 2005 5:43 PM

 
Anonymous Anonymous said...

Wil,

Do you have some sort of inside knowledge (besides the existence of the compiler flag) that Apple wants to change the runtime semantics for everyone?

In many many places through the documentation, it says a message to nil is valid, and you can use the result as long as it returns a scalar integer type whose size is <= sizeof(void *).

http://www.google.com/search?q=site:apple.com%20message%20to%20nil

>>>
A message to nil also is valid, as long as the message returns an object, any pointer type, or any integer scalar of size less than or equal to sizeof(void*); if it does, a message sent to nil returns nil. If the message sent to nil returns anything other than the forementioned value types (for example, if it returns any struct type, any floating-point type, or any vector type) the return value is undefined. In general, however, it is considered bad style to rely on this behavior for return types other than an object.
<<<

I can see how in certain translation units where you have complete control over a small chunk of code, enabling that flag might enable performance optimizations, but a blanket deprecation of being able to message nil would break tons and tons of code.

I'd have to make all my dealloc methods more verbose

- (void)dealloc
{
if (thing != nil)
[thing release];
[super dealloc];
}

vs.

- (void)dealloc
{
[thing release];
[super dealloc];
}

October 09, 2005 6:05 PM

 
Anonymous Jonathan Johnson said...

Unfortunately, no, typeof cannot be used in a #if statement. It adheres to the same limitations of sizeof() in that #if, #define, and so forth are handled at a stage before any of the types are known about, and thus sizeof() and typeof() can't be determined.

Although, I've heard that part of gcc-4 is that they switched to a custom parser instead of a grammar-based parser for C and C++, which theoretically means that they could alleviate this limitation.

However, the other problem is that typeof()'s return type can't be used in a comparison. I'm not sure what it returns, but it seems to only be valid in the situation of:

#define Clone( a ) ((a) ? a->Clone() : (typeof(a))NULL)

While that example is rather silly, it's more useful for template classes in C++ it seems -- in all honesty, I've never used it except for today to see if I could come up with a way to do a comparison on results for this competition ;)

October 09, 2005 6:12 PM

 
Anonymous Jonathan Johnson said...

Well I can't do it use it macros, btu there is a trick to get around it if you're willing to compile that one file as Objective-C++ instead of just Objective-C.

Unfortunately, that's not quite true -- any file that includes the header for the overloaded methods must also be Objective-C++ as well.

October 09, 2005 6:19 PM

 
Anonymous Anonymous said...

Actually, nil is synonymous with 0.0f. The problem is, on i386, a float as return value gets looked up at a different position (similar like a struct is on ppc). And the value there is not synonymous with 0.0f.
OTOH, just taking memory filled with nils and interpreting it as float* yields lots of 0.0fs.

October 10, 2005 1:41 AM

 
Blogger Wil Shipley said...

Well, the effect is, you can't send a message to nil and expect it to return 0.0f. Which I remember from the Old Days, when we used to run on Intel processors the first time.

October 10, 2005 1:53 AM

 
Blogger f(n) said...

Okay, I think I deserve to win. As others have pointed out, there is no static way to check the dynamic type of an Objective-C object, but my code does meet the goal of eliminating the overhead of -respondsToSelector and greatly improves performance.

First, I want to point out an error in your original code, Wil. You tried to be overly l337 and in writing your code, you made a mistake. Quite simply, what happens if you have an object that responds to -length but doesn't have a length of zero? Your AND clause fails and you go on to test it for a -count method, even though you already have your answer (the object isn't empty). The problem is that if the object responds to -length, you don't want to test it for -count regardless of whether the length is zero or not. I don't think you can do this with binary operators. I prefer to just write out the if..else if statement as it is more readable, but if you must write it in l337, you'll need to use the ternary ?: operator (I've included both versions in my code).

As for my improved solution, it relies on knowledge of the runtime. I simply cache the memory location for the class object of each known type to be checked and then fall back to your code if it doesn't fit one of the known types. It is fairly easy to extend, just copy one of the get*Class functions and add code to the appropriate (length or count) function where noted. My code is about twice as fast for every case except a nil object, where it is a tad bit slower.

I didn't bother with prototypes, so it is probably more readable from bottom to top and I had to change the angle brackets on my import statement. I haven't completely checked it for bugs and it is late (early), so I reserve the right to change it. But here it is:

/*
* DMCommonMacros.h
* IsEmptyPP
*
* Created by Nathan Florea on 10/10/05.
*
*/

#import #objc/objc-class.h#

// Makes sure I am getting the right class and
// not a lower-level concrete class
Class getClass(id thing, const char *className) {
Class thingClass = thing->isa;
size_t classNameLen = strlen(className);
do {
if (strncmp(thingClass->name, className, classNameLen) == 0)
return thingClass;
} while (thingClass = thingClass->super_class);
return NULL;
}

// Gets the class object for NSData.
// I've broken these out so that the
// inlined code is a bit more streamlined.
// Theoretically, this should have less bloat.
// It seems like there should be a function or
// method that would take a string of a class
// name and return the Class object, but I
// didn't find it.
Class getNSDataClass() {
Class NSDataClass;
NSData *tempData = [NSData alloc];
NSDataClass = getClass(tempData, "NSData");
[tempData dealloc];
return NSDataClass;
}

Class getNSStringClass() {
Class NSStringClass;
NSString *tempString = [NSString alloc];
NSStringClass = getClass(tempString, "NSString");
[tempString dealloc];
return NSStringClass;
}

Class getNSArrayClass() {
Class NSArrayClass;
NSData *tempArray = [NSArray alloc];
// Interestingly, if I don't -init my array
// it is of type NSPlaceholderArray
NSArrayClass = getClass(tempArray, "NSArray");
[tempArray dealloc];
return NSArrayClass;
}

static inline BOOL definitelyRespondsToLength(id thing) {
// Add a static class variable here for additional
// classes that respond to length.
static Class NSDataClass = 0;
static Class NSStringClass = 0;
if (NSDataClass == 0) { // We only need to check one static variable
// Initialize the variable here.
NSDataClass = getNSDataClass();
NSStringClass = getNSStringClass();
}
Class thingClass = (Class)thing->isa;
do {
// Add a check here for additional classes.
if (thingClass == NSDataClass || thingClass == NSStringClass)
return true;
thingClass = (Class)thingClass->super_class;
} while (thingClass);
return false;
}

static inline BOOL definitelyRespondsToCount(id thing) {
// Add a static class variable here for additional
// classes that respond to count.
static Class NSArrayClass = 0;
if (NSArrayClass == 0) { // We only need to check one static variable
// Initialize the variable here.
NSArrayClass = getNSArrayClass();
}
Class thingClass = (Class)thing->isa;
do {
// Add a check here for additional classes.
if (thingClass == NSArrayClass)
return true;
} while (thingClass = (Class)thingClass->super_class);
return false;
}



static inline BOOL IsEmpty(id thing) {
return thing == nil
|| ([thing respondsToSelector:@selector(length)]
&& [(NSData *)thing length] == 0)
|| ([thing respondsToSelector:@selector(count)]
&& [(NSArray *)thing count] == 0);
}

static inline BOOL IsEmptyImproved(id thing) {
if (thing == nil)
return TRUE;
else if ([thing respondsToSelector:@selector(length)])
return ([thing length] == 0);
else if ([thing respondsToSelector:@selector(count)])
return ([thing count] == 0);
// We can't tell, but we know it isn't nil
return FALSE;
}

static inline BOOL IsEmptyImprovedl33t(id thing) {
return thing == nil
|| [thing respondsToSelector:@selector(length)] ?
([(NSData *)thing length] == 0) :
([thing respondsToSelector:@selector(count)]
&& [(NSArray *)thing count] == 0);
}

static inline BOOL IsEmptyPP(id thing) {
if (thing == nil)
return TRUE;
else if (definitelyRespondsToLength(thing))
return ([thing length] == 0);
else if (definitelyRespondsToCount(thing))
return ([thing count] == 0);
else if ([thing respondsToSelector:@selector(length)])
return ([thing length] == 0);
else if ([thing respondsToSelector:@selector(count)])
return ([thing count] == 0);
// We can't tell, but we know it isn't nil
return FALSE;
}

October 10, 2005 4:41 AM

 
Blogger f(n) said...

Here are my performance numbers. I ran tests with six different objects, testing each codepath (okay, not entirely on mine; I don't have an object that responds to -length or -count but misses my cached classes). I just looped through 20 million times for each test using the same object. Not the best methodology, but it gives rough numbers. Also, I am just measuring elapsed time rather than computational time, so that introduces some error as well.
The six objects I used were nil, an empty NSString (responds to -length, same as NSData), a non-empty NSString, an empty NSArray (responds to -count), a non-empty NSArray, and an NSScanner (worst case). I used three versions of the macro: IsEmpty(), Wil's original version; IsEmptyImproved(), Wil's original version with the logic error fixed (IsEmptyImprovedl33t performs about the same); and IsEmptyPP(), my version.

The results (all times are in seconds):

Times for nil test:
IsEmptyPP was 0.455089
IsEmptyImproved was 0.450769
IsEmpty was 0.442453

Times for empty string test:
IsEmptyPP was 1.622143
IsEmptyImproved was 3.206602
IsEmpty was 3.214261

Times for non-empty string test:
IsEmptyPP was 2.077117
IsEmptyImproved was 3.711981
IsEmpty was 5.695658

Times for empty array test:
IsEmptyPP was 3.213525
IsEmptyImproved was 6.162985
IsEmpty was 6.154492

Times for non-empty array test:
IsEmptyPP was 3.428324
IsEmptyImproved was 6.025952
IsEmpty was 6.081842

Times for NSScanner test:
IsEmptyPP was 5.749863
IsEmptyImproved was 4.547361
IsEmpty was 4.604802

October 10, 2005 5:07 AM

 
Blogger f(n) said...

And here is the code I used for the performance test. I should also do some correctness tests, but that is the boring stuff that will wait until tomorrow (today).

Code:

#import #Foundation/Foundation.h#
#import "DMCommonMacros.h"
#import #sys/time.h#

#define MAX_TESTS 20000000
#define MIN_TESTS 20

int runTest(id testObject) {
int i;
struct timeval startTime, finishTime;

gettimeofday(&startTime, NULL);
for (i = 0; i < MAX_TESTS; i++)
IsEmptyPP(testObject);
gettimeofday(&finishTime, NULL);
timersub(&finishTime, &startTime, &finishTime);
printf("IsEmptyPP was %i.%06i\n", finishTime.tv_sec, finishTime.tv_usec);

gettimeofday(&startTime, NULL);
for (i = 0; i < MAX_TESTS; i++)
IsEmptyImproved(testObject);
gettimeofday(&finishTime, NULL);
timersub(&finishTime, &startTime, &finishTime);
printf("IsEmptyImproved was %i.%06i\n", finishTime.tv_sec, finishTime.tv_usec);

gettimeofday(&startTime, NULL);
for (i = 0; i < MAX_TESTS; i++)
IsEmpty(testObject);
gettimeofday(&finishTime, NULL);
timersub(&finishTime, &startTime, &finishTime);
printf("IsEmpty was %i.%06i\n", finishTime.tv_sec, finishTime.tv_usec);

printf("\n");

return 0;
}


int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString *testString = [NSString stringWithCString:"New String"];
NSString *emptyString = @"";
NSArray *testArray = [NSArray arrayWithObject:testString];
NSArray *emptyArray = [NSArray array];
NSScanner *testScanner = [NSScanner scannerWithString:testString];
int i;

// Make sure all code is paged in, no advantages in going last
for (i = 0; i < MIN_TESTS; i++)
IsEmpty(testArray);
for (i = 0; i < MIN_TESTS; i++)
IsEmptyImproved(testArray);
for (i = 0; i < MIN_TESTS; i++)
IsEmptyPP(testArray);
// I am eliminating my overhead, but it would be to big a pain
// to try to run every test cold (a lot of recompiles).
// I could calculate the overhead by running this twice and
// comparing times, but I don't really care.


// Nil
printf("Times for nil test:\n");
runTest(nil);

// Empty string
printf("Times for empty string test:\n");
runTest(emptyString);

// Non-empty string
printf("Times for non-empty string test:\n");
runTest(testString);

// Empty array
printf("Times for empty array test:\n");
runTest(emptyArray);

// Non-empty array
printf("Times for non-empty array test:\n");
runTest(testArray);

// NSScanner test
printf("Times for NSScanner test:\n");
runTest(testScanner);


[pool release];
return 0;
}

October 10, 2005 5:09 AM

 
Anonymous Anonymous said...

Here's my shot at it:

static id nsDataIsa, nsArrayIsa;
static IMP nsDataImp, nsArrayImp;

extern id objc_msgSend(id theReceiver, SEL theSelector, ...);

static id (*priv_objc_msgSend)( id p, SEL sel, ...) = objc_msgSend;

/* call this before doing anything */
static void inline initImps(void)
{
id data = [[NSData alloc] init];
id array = [[NSArray alloc] init];

nsDataImp = [data methodForSelector:@selector(length)];
nsArrayImp = [array methodForSelector:@selector(count)];
nsDataIsa = data->isa;
nsArrayIsa = array->isa;

[data release];
[array release];
}

struct priv_objc_class {
struct objc_class *isa;
struct objc_class *super_class;
};

static inline BOOL IsEmpty2(id thing)
{
if (thing == nil)
return nil;
else if (thing->isa == nsArrayIsa)
return (nsArrayImp(thing, @selector(count)) == 0);
else if (thing->isa == nsDataIsa)
return (nsDataImp(thing, @selector(length)) == 0);
else if (((struct priv_objc_class *)(thing->isa))->super_class == nsArrayIsa)
return priv_objc_msgSend(thing, @selector(count)) == 0;
else if (((struct priv_objc_class *)(thing->isa))->super_class == nsDataIsa)
return priv_objc_msgSend(thing, @selector(length)) == 0;

return NO;
}

Cheers,

M Tabbara.

October 10, 2005 5:15 AM

 
Blogger f(n) said...

You can download both of my files directly from here.

M. Tabbara:
A couple of comments.

First, why are you creating a pointer to objc_msgSend? I don't understand the logic there and I wonder if I am missing something.

Second, why the private Class structure? Again, I am not following the logic. Are you avoiding a #import for some reason?

Third, it would be nice to initialize your static variables from within the macro so that the user doesn't have to make multiple calls or worry about whether or not your macro is initialized.

And finally, you'll probably find that checking the object's class and super class isn't as robust as you would hope. For example, the class hierarchy for [NSData dataWithData:someData] is NSConcreteData->NSData->NSObject whereas for [NSMutableData dataWithData:someData] it is NSConcreteMutableData->NSMutableData->NSData->NSObject. And then there are subclasses....

October 10, 2005 6:06 AM

 
Anonymous M. Tabbara said...

Ooops, that last return NO should have been a return YES.

The code only covers NSArray, NSData and any direct subclasses, returning YES (empty) otherwise or when the object is nil. The if/else are are ordered in "priority". i.e. the fast case is when the object is nil, then NSArray, NSData, direct subclasses of NSArray, direct subclasses of NSData, all else. I probably should have fallen back to Wil's code before just giving up and returning YES.

Ideally, the classe's isa pointer could be used as the key to index into a hash table returning the IMP that counts the size but I got lazy. It should also be possible to construct a "perfect" hash since the number of classes that you want to have IMPs for is known at compile time and the offsets between each class' isa is constant.

Cheers,

M. Tabbara.

October 10, 2005 6:14 AM

 
Anonymous M. Tabbara said...

Hi Nathan,

The pointer to objc_msgSend means I always call via the function (pointer) instead of the dyld stub code which is a reasonably big saving.

Having an init function (only need to call this once in main.m) means I avoid testing whether they've been initialized or not.

I agree about robustness and I think the way to solve that is to write a small program that runs through the object heirarchy, catalogs classes that respond to length/count, calculates offsets between the (lowest/highest) first isa of such a class and every subsequent such class, writes and appropriate (one-to-one) hash function where the index is the isa offset and key is the IMP (to be filled in at run-time).

October 10, 2005 6:25 AM

 
Anonymous Toby Paterson said...

Here's a version that doesn't require ObjC++, runtime trickery, objc_msgSend() and is only about 8 lines of (real) code.

#import <Foundation/Foundation.h>

BOOL ___IsEmpty (id x, char *xtype)
{
BOOL isEmpty = NO;
if ('{' == xtype[0] && 'N' == xtype[1] && 'S' == xtype[2] && 'S' == xtype[3] && 't' == xtype[4] && 'r' == xtype[5] && 'i' == xtype[6] && 'n' == xtype[7] && 'g' == xtype[8] && '=' == xtype[9]) {
isEmpty = (0 == [(NSString *)x length]);
NSLog(@"you've got a string and it is%@ empty (%@)", isEmpty ? @"" : @" not", x);
} else if ('{' == xtype[0] && 'N' == xtype[1] && 'S' == xtype[2] && 'A' == xtype[3] && 'r' == xtype[4] && 'r' == xtype[5] && 'a' == xtype[6] && 'y' == xtype[7] && '=' == xtype[8]) {
isEmpty = (0 == [(NSArray *)x count]);
NSLog(@"you've got an array and it is%@ empty (%@)", isEmpty ? @"" : @" not", x);
} else {
NSLog(@"you've got something else and I don't know if it's empty or not, so I'm going to say NO! (%@)", x);
}
return isEmpty;
}

#define IsEmpty(x) ((nil == x) || ___IsEmpty(x, @encode(typeof(*x))))

main()
{
[NSAutoreleasePool new];

id i = @"Hello, world";
if (IsEmpty(i)) {
NSLog(@"i is empty");
}

NSString *s = @"How old do you think I am?";
if (IsEmpty(s)) {
NSLog(@"s is empty");
}

NSArray *a = [NSArray array];
if (IsEmpty(a)) {
NSLog(@"a is empty");
}

NSArray *n = nil;
if (IsEmpty(n)) {
NSLog(@"n is empty");
}
}

Extending it to cover mutable types, NSData, etc. is left as an exercise for the reader.

I'm doing the direct comparison of the type string to avoid a function call to strcmp(). Doesn't generate the most efficient code, but it's still cheaper than isKindOf: or respondsToSelector:. It can be optimized a bit by doing the comparison a word at a time or by playing some games with string uniquing.

Does that qualify for the sock puppet?

October 10, 2005 6:39 AM

 
Anonymous M. Tabbara said...

Hi Toby,

Using @encode like this will only work if you knew the type at compile time. i.e.

id someArray = [[NSArray alloc] initWithObjects: @"foo", nil];
isEmpty(someArray) will return YES.

October 10, 2005 7:02 AM

 
Blogger f(n) said...

M. Tabbara,
Avoiding the dyld stub sounds neat. I'll have to check it out.
How about priv_objc_class?

I think you were right the first time to return NO. If the object isn't nil and you don't know how to check if it is empty or not (e.g. NSScanner), you are probably safer assuming it is not empty rather than that it is. Although I suppose it depends on how you are using it.

October 10, 2005 7:14 AM

 
Anonymous Toby Paterson said...

Hi M. Tabbara. Sure; you can always fall back on respondsToSelector: if you want.

October 10, 2005 7:21 AM

 
Anonymous Karl Adam said...

This code is all an awesome example of why you don't want to over optimize. Now a single one of these solutions is the slightest bit easy to read nor friendly.

A couple things:
1) ObjC++ code can be used freely in ObjC code. It's one of the reasons why ObjC++ is such an amazing achievement.

2) It's true that the compiler alone can never solve this problem simply because type information is entirely reliant on the ObjC runtime.

3) isKindOfClass: and respondsToSelector: actually aren't very alike at all. isKindOfClass: only has to travel up the super_class definitions on objc_class. While respondsToSelector: iterates over the method_lists member since all the methods it responds to are locally available, no traveling up the inheritance dictionary. I'd wager that respondsToSelector: is a much more expensive call than isKindOfClass:

4) Nathan, as 1337 as you think you're solution is, it fixes something that wasn't broke. The C compiler does boolean short-circuiting where in an || operation if one part of that chain evaluates to true, it is done and doesn't evaluate the remaining components.

5) Nathan, your solution is also quite fragile since it relies on implementation details that can change in the future. Most notably the check for whether two classes have the same name. ObjC only creates one instance of each class object so you can more safely just check whether they equal each other.

6) M. Tabbara, in class clusters primitive methods like -count are supposed to be overridden so if you cache the selector for the initted data object then you might be using the wrong implementation for the current object.

7) Toby, your code makes me cry.

So, given the choice of any of these implementations, I would pick Wil's original solution everytime. The logic is obvious, it's short, intuitive, and A LOT easier to read then everything else on this page.

October 10, 2005 7:37 AM

 
Anonymous M. Tabbara said...

Hi Karl,

1. ObjC++ means you still need to compile ObjC++. You can't just start writing C++ code in a .m file.
2. The suggestion that C++ is somehow useful because of overloading and/or template specialization. Again, this relies on knowing the type at compile time.
3. The code I punched out isn't meant to be a complete solution. Using a utility invoked to traverse every class in Foundation/AppKit that has a length or count method is do-able and then have the same utility genereate code similiar to what I wrote. To avoid the if/elses, a hashtable where you hash the isa pointer will further improve things especially if there are a lot of classes that respond to length/count.
4. For the record, I'm caching method implementations (IMPs) rather than selectors. Since selectors are known at compile time, there's not much point to caching them at runtime.
5. Finally, IMHO, if isEmpty is called frequently enough, there is value in looking at (robust) optimizations.

October 10, 2005 8:04 AM

 
Anonymous Greg Titus said...

All these optimizations are insane. If you don't want to use Wil's original solution because of the extra -respondsToSelector:, then don't use a generic method/macro/function at all.

99.999% of the time you know whether you are dealing with a string/data or array/dictionary so if you care about the extra little bit of speed, why aren't you just using if(![string length]) or if (![array count])?

The whole point originally, I thought, was to _simplify_!

October 10, 2005 8:14 AM

 
Anonymous greg titus said...

Also, if you are calling isEmpty() a billion times, either you have a billion different objects you are calling it on, in which case you are going to be killed by memory performance and nothing you do with the call itself is going to help, or else you are calling it many many times on the same objects, in which case: optimize that instead of messing with the isEmpty() implementation. Cache the result! Rearrange the computation so you only have to dete
rmine whether something is empty once, and throw out the empties or non-empties or whatever is appropriate so you don't have to look at them again.

This is just the wrong level to worry about performance of this kind of thing.

October 10, 2005 8:25 AM

 
Blogger f(n) said...

That's funny, Greg, I thought the point was to win a Delicious Monster sock monster. : )
Keep in mind, we aren't doing this instead of working on Delicious Monster or something. This was just a fun puzzle that Wil posed and we're taking shots at it. This is 100% of the code we are dealing with, so there is no optimizing at a different level. This is the only level for the purposes of this contest.

Karl,
4) I don't think you understand the problem. Read it again. The problem is that one of the OR clauses is evaluating to zero and not short-circuiting when you would want it to (i.e., when an object responds to -length but isn't empty). I really don't think either of my versions are that much harder to read, since one uses the same terse operators as Wil's, but produces the corrrect behavior (I think) and the other one rewrites Wil's code in a more readable and correct way.
I don't think my code is l337; that was a joke.

5) Again, I think you should read more carefully. I don't believe I check whether two classes have the same name anywhere. What I do do is check a class name against a string representing the class name I want. The problem is that an NSData isn't really an NSData, it is an NSConcreteData. So if I want to get to the NSData class object, I have to climb up the inheritance hierarchy until I find the class with that name. Perhaps that is what you mistook for comparing two classes for equal names?

Yes, my solution is somewhat fragile. However, it relies on details that haven't changed since OS X was around (and I am pretty sure well before) and it is pretty likely it won't change in the future. If it did, you would have to make some changes to the details, obviously, but I think the overall design is sound. There will always be a runtime and it is hard to get a dynamic one more bare than this.

o, given the choice of any of these implementations, I would pick Wil's original solution everytime. The logic is obvious, it's short, intuitive, and A LOT easier to read then everything else on this page.
It is also flawed.

October 10, 2005 8:40 AM

 
Anonymous Jussi said...

This discussion just proves the point Wil has been making all along, less code is better. Simpler code is better. And don't freaking optimize prematurely or try to do some 1337 trickery!

I think Nathan's point about the slight logical problem is valid and also that it is usually easier to read if-then structures than long boolean expressions.

This kind of blog entries and discussion is very welcome and needed in the Cocoa -community, it is always a great read!

October 10, 2005 8:59 AM

 
Blogger f(n) said...

Thanks, Jussi. I don't want to puff it up, it is a slight error, and it doesn't affect the correctness of the code, it just slows things down for certain cases.

Anyway, it is all in good fun. And I am really sorry I ever mentioned "l337"; it is the Worst. Slang. Ever.

October 10, 2005 9:09 AM

 
Blogger Mirko Viviani said...

You can use GCC __builtin_ functions to know the type of a variable and run the right code for that object.
It's a bit tricky and totally unreadable but it seems to do the job.

Ciao
Mirko

/*
* DMCommonMacros.h
*
* Created by Mirko Viviani on 10/10/05.
*
*/

#include #Foundation/Foundation.h#


#define IsEmpty_count(thing) \
( thing == nil ? YES : [(NSArray *) thing count] == 0 )
#define IsEmpty_length(thing) \
( thing == nil ? YES : [(NSString *) thing length] == 0 )
#define IsEmpty_general(thing) \
( thing == nil ? YES \
: ([thing respondsToSelector:@selector(length)] \
? [(NSData *) thing length] == 0 \
: ([thing respondsToSelector:@selector(count)] \
? [(NSArray *) thing count] == 0 : YES)))

#define IsEmpty(thing) \
( \
__builtin_choose_expr( \
__builtin_types_compatible_p(__typeof__(thing), NSArray *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSMutableArray *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSSet *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSMutableSet *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSSet *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSDictionary *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSMutableDictionary *), \
IsEmpty_count(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSString *), \
IsEmpty_length(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSMutableString *), \
IsEmpty_length(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSData *), \
IsEmpty_length(thing), \
__builtin_choose_expr ( \
__builtin_types_compatible_p(__typeof__(thing), NSMutableData *), \
IsEmpty_length(thing), \
IsEmpty_general(thing)))))))))))))

October 10, 2005 10:04 AM

 
Anonymous Karl Adam said...

Nathan, you're right, I did read it the wrong way, so you are correct in that it does have that minor flaw, however that's not big enough to switch to any of the other implementations here.

You do compare the names of two classes, right here:
if (strncmp(thingClass->name, className, classNameLen) == 0)
Sure, technically you're merely comparing the name to see if it's the class that you are looking for, but since you needed the original class in the first place to get the name, you're basically comparing two classes by name.

Also, I assumed you were joking about 1337, after all 1337 is sooo last year.

marko: your code makes me cry almost as much as toby's did.


M. Tabbara: You can compile one or two files as ObjC++, and the rest of your project as ObjC and use the code from either end. I have this setup in one of my personal projects. You can go as far as compiling and running java code in an ObjC application along with ObjC++. That is what a certain development branch of adium currently has going on. This all works because the ObjC runtime is... for lack of better words, FREAKING AWESOME. Also, I mistyped when I said selectors, since that wouldn't be a problem at all. The problem is the IMPs since as I said, subclasses in class clusters are supposed to override "primitive" methods like count/length. Basically you've cached an implementation that isn't going to work for any given subclass of that type. A selector doesn't have that problem since it's corresponding IMP would be looked up by the runtime. And lastly maintaining a hastable of isas(obj_class structs) is silly since that's the job of the runtime, and I'd wager it's better at looking up those objc_class structs than you could be with your cache of them.


Yes, more implementations would make it more rubust, uglier implementations can make it faster, BUT in millions of operations you guys are showing gains in mere seconds. What about Wil's attitudes towards coding would suggest that a few seconds shaved in MILLIONS of runs would outweigh the value in shorter, easier to read, and much more robust solutions? Now if you guys had an implementation that took less time, was easier to read, and did more, then I would have copy/pasted it and ran off with it hours ago. Instead we seem to have past the point of simpler solutions, the only one being Nathan's "1337" version, and now we are in the horribly ugly solutions that require way too much programmer time to grok, much less fix when the day comes that something goes wrong with it since compiler details change, implementation details change, and even language and API details can change.

Said another way, less IS more, and Wil's solution still bears out as the winner amongst everything on this page.

P.S. Wil needs to figure out some way to get posted code to prettify on here, or give us the magic needed to prettify our code.

October 10, 2005 11:48 AM

 
Anonymous Adam said...

My understanding of NSString is that [string length] is not a valid check for an empty string: you need to use -[NSString isEqualToString:@""]. There is a note on this in OmniFoundation's +[NSString isEmptyString:], and I also found this message in the cocoa-dev archives: http://lists.apple.com/archives/cocoa-dev/2004/Jul/msg00166.html. Just something to keep in mind...

October 10, 2005 12:31 PM

 
Anonymous greg titus said...

Right, sock puppet. Sorry. :-)

I forgot the real purpose.

October 10, 2005 12:58 PM

 
Blogger f(n) said...

Karl Adam,
Sorry to belabor the point, but I am still not getting what you mean by saying that I had to have the original class to get the name. The name is a string constant in the code, not something I am getting programmatically, and the problem is that what I have is, for instance, an instance of NSCFArray, which is three subclasses down from what I really want, NSArray. When do I have an NSArray instance? If you can show me a better way to do that, then please demonstrate. But otherwise I don't know what you are talking about.

Hmm, looking at it, I suppose I could do [NSArray class], which is much cleaner. However, I don't think that is what you were talking about and with the existing method, I don't see a better way of doing it.

October 10, 2005 1:23 PM

 
Blogger f(n) said...

Okay, here is my much improved version. Also, thanks to Adam, this is correct for Unicode strings, whereas previous checks were not. It is interesting to note that while -isEqualToString:@"" is almost as efficient for an empty string as -length, it is much slower for non-empty strings. Apparently there is some optimization that could occur there. I bet they check memory location first and quickly exit for equal constant strings, but it doesn't seem like a string that is different on the first character should take twice as long. Of course, even that case only takes 75% of the time as Wil's original version.

Here's the code:
static inline BOOL IsEmptyPPImproved(id thing) {
static Class NSArrayClass = 0;
static Class NSDataClass = 0;
static Class NSStringClass = 0;
static Class thingClass = 0;
if (NSArrayClass == 0) {
NSArrayClass = [NSArray class];
NSDataClass = [NSData class];
NSStringClass = [NSString class];
}

if (thing == nil)
return TRUE;
thingClass = (Class)thing->isa;
do {
if (thingClass == NSArrayClass)
return ([(NSArray *)thing count] == 0);
else if (thingClass == NSDataClass)
return ([(NSData *)thing length] == 0);
else if (thingClass == NSStringClass)
return ([(NSString *)thing isEqualToString:@""]);
} while (thingClass = thingClass->super_class);

if ([thing respondsToSelector:@selector(length)])
return ([thing length] == 0);
else if ([thing respondsToSelector:@selector(count)])
return ([thing count] == 0);
// We can't tell, but we know it isn't nil
return FALSE;
}

It is pretty easy to extend as well. I really like this version. Performance numbers to follow.

October 10, 2005 1:43 PM

 
Blogger f(n) said...

I tested Wil's original, my original improvement (IsEmptyPP) and my newer version (IsEmptyPPImproved)
Here are the numbers (all times in seconds):
Times for nil test:
IsEmptyPP was 0.448315
IsEmptyPPImproved was 0.444624
IsEmpty was 0.447291

Times for empty string test:
IsEmptyPP was 1.576948
IsEmptyPPImproved was 2.040065
IsEmpty was 3.195135

Times for non-empty string test:
IsEmptyPP was 2.093524
IsEmptyPPImproved was 4.326613
IsEmpty was 5.736067

Times for empty array test:
IsEmptyPP was 3.370103
IsEmptyPPImproved was 2.671988
IsEmpty was 6.100892

Times for non-empty array test:
IsEmptyPP was 3.355505
IsEmptyPPImproved was 2.663617
IsEmpty was 6.049525

Times for NSScanner test:
IsEmptyPP was 5.602919
IsEmptyPPImproved was 5.207016
IsEmpty was 4.545897

I was a little surprised by the substantial gains for NSArrays and that the worst case improved quite a bit. I wish I would have submitted this version originally, but it is really just an evolution of my original design. Perhaps if I had slept some....

October 10, 2005 1:49 PM

 
Blogger f(n) said...

Karl said:
BUT in millions of operations you guys are showing gains in mere seconds.

Again, Wil didn't ask for a more efficient version because he is trying to speed up his code. He asked for it because it is a fun little exercise and it is good practice. Obviously, in the real world, you would profile your app, take the 20% of your code that sucks up 80% of the execution time, and apply the techniques we are using here to that code.
Also, when optimizing code, time measurements are mostly meaningless. What is important is the relative performance. My code is over 2 times faster for some common cases, which is a pretty nice increase. Of course this code is going to be a tiny, tiny, inconsequential fraction of any app, but that isn't the point of this mental exercise. It is simply that, exercise.

October 10, 2005 2:09 PM

 
Blogger Mirko Viviani said...

Karl,

I know that less code is better, but if you want to optimize you have to pay something.
The point is: is it really necessary?

Anyway this is a more readable version:

#define IsEmpty_count(thing) \
( thing == nil ? YES : [(NSArray *) thing count] == 0 )

#define IsEmpty_length(thing) \
( thing == nil ? YES : [(NSString *) thing length] == 0 )

#define IsEmpty_string(thing) \
( thing == nil ? YES : [(NSString *) thing isEqualToString: @""] )

#define IsEmpty_general(thing) \
( thing == nil ? YES \
: ([thing respondsToSelector:@selector(length)] \
? [(NSData *) thing length] == 0 \
: ([thing respondsToSelector:@selector(count)] \
? [(NSArray *) thing count] == 0 : YES)))

#define __TEST_TYPE__(thing, type, function) \
__builtin_choose_expr( \
__builtin_types_compatible_p(__typeof__(thing), type), \
function(thing)

#define IsEmpty(thing) \
( \
__TEST_TYPE__(thing, NSArray *, IsEmpty_count), \
__TEST_TYPE__(thing, NSMutableArray *, IsEmpty_count), \
__TEST_TYPE__(thing, NSSet *, IsEmpty_count), \
__TEST_TYPE__(thing, NSMutableSet *, IsEmpty_count), \
__TEST_TYPE__(thing, NSDictionary *, IsEmpty_count), \
__TEST_TYPE__(thing, NSMutableDictionary *, IsEmpty_count), \
__TEST_TYPE__(thing, NSString *, IsEmpty_string), \
__TEST_TYPE__(thing, NSMutableString *, IsEmpty_string), \
__TEST_TYPE__(thing, NSData *, IsEmpty_length), \
__TEST_TYPE__(thing, NSMutableData *, IsEmpty_length), \
IsEmpty_general(thing))))))))))))


__builtin_ strength is that they are evaluated at compile time but they are available only on gcc and sometimes have bugs. (see __builtin_apply() with float/double if I remember correctly)
__builtin_choose_expr(COND, EXPTRUE, EXPFALSE) is the same as the ?: expression and its readability with nested expressions is the same... ie none!
You can convert IsEmpty_*() macro to static inline if you prefer.

Anyway these are some results of my tests.
Wil IsEmpty uses isEqualToString: instead of length. (same function)

- (void) testTimeArray
{
NSTimeInterval start, stop;
int i;

start = [NSDate timeIntervalSinceReferenceDate];
for (i = 0; i < 10000000; i++)
IsEmpty(arrayTest);
stop = [NSDate timeIntervalSinceReferenceDate];
NSLog(@"%.3f seconds", stop - start);
}

These are tests results with optimizations turned off (-O0):

2005-10-10 23:46:55.695 otest[14898] 1.167 seconds
Test Case '-[OLCommonMacrosTest testTimeArray]' passed (1.180 seconds).
2005-10-10 23:46:59.717 otest[14898] 4.005 seconds
Test Case '-[OLCommonMacrosTest testTimeArrayWil]' passed (4.021 seconds).
2005-10-10 23:46:59.872 otest[14898] 0.121 seconds
Test Case '-[OLCommonMacrosTest testTimeNil]' passed (0.136 seconds).
2005-10-10 23:47:00.335 otest[14898] 0.444 seconds
Test Case '-[OLCommonMacrosTest testTimeNilWil]' passed (0.461 seconds).
2005-10-10 23:47:07.683 otest[14898] 7.315 seconds
Test Case '-[OLCommonMacrosTest testTimeString]' passed (7.328 seconds).
2005-10-10 23:47:18.005 otest[14898] 10.291 seconds
Test Case '-[OLCommonMacrosTest testTimeStringWil]' passed (10.305 seconds).


These are results with optimizations (-O2):

2005-10-10 23:45:22.546 otest[14773] 1.008 seconds
Test Case '-[OLCommonMacrosTest testTimeArray]' passed (1.026 seconds).
2005-10-10 23:45:26.041 otest[14773] 3.460 seconds
Test Case '-[OLCommonMacrosTest testTimeArrayWil]' passed (3.475 seconds).
2005-10-10 23:45:26.095 otest[14773] 0.035 seconds
Test Case '-[OLCommonMacrosTest testTimeNil]' passed (0.050 seconds).
2005-10-10 23:45:26.156 otest[14773] 0.030 seconds
Test Case '-[OLCommonMacrosTest testTimeNilWil]' passed (0.075 seconds).
2005-10-10 23:45:33.853 otest[14773] 7.636 seconds
Test Case '-[OLCommonMacrosTest testTimeString]' passed (7.652 seconds).
2005-10-10 23:45:43.634 otest[14773] 9.748 seconds
Test Case '-[OLCommonMacrosTest testTimeStringWil]' passed (9.767 seconds).

Ciao
Mirko

October 10, 2005 3:08 PM

 
Anonymous Karl Adam said...

Nathan, this version is MUCH better to look at though it has a couple issues. First off you initialize with 0 instead of nil. Yes, they are the same, but the compiler does not treat nil, NULL, and 0 the same. ObjC++ constantly reminds me of this fact.

You use a stack of else ifs and a temp variable as your control, but that requires several conditions be run. You could reduce it to one evaluation with a switch on object->isa. Also you then compare the returned values of count and length to 0, you could instead just ! the returned value, since it removes a comparison and will do functionally the same thing. Yes, these are minor and tiny optimizations but they can be made while keeping the readability clean.

As to the original, even with having the string beforehand, strncmp() is an O(n) operation, whereas == is O(1), matter of fact it's so trivial it's barely an instruction, so my suggestion was/is win/win.

October 10, 2005 4:32 PM

 
Blogger f(n) said...

Karl,
Okay, now I understand. I wasn't sure that the strings would be located in the same place in memory. Are you sure they are? I know NSString is very aggressive about not duplicating strings, but it seemed like a compile-time constant and a runtime variable would be located in different areas of memory. Does the runtime search through existing string constants when initializing?

Now I don't get how I would use a switch. First, case labels have to be constant expressions and the variables I use in my conditions (like NSDataArray) are not. Second, if I get rid of my temp variable, how do I cycle through thing's superclasses?

And what is the point of initializing to nil? I thought nil represented a NULL object while these are pointers to objc_class structures. Wouldn't that be just a style difference and semantically incorrect is this case?

Hmm, I would have thought the compiler would optimize the NOT operator and a comparison to 0 to pretty much the same (a single instruction). I'll have to check the compiler's output.

Thanks for the suggestions and critiques; even the ones I don't agree with. : )

October 10, 2005 5:32 PM

 
Anonymous Marcel Weiher said...

I agree with Lucas that a category is the most elegenat solution, and it works absolutely fine with nil-messaging as long as you reverse the polarity of the question asked:

-(BOOL)isNotEmpty;

October 10, 2005 5:42 PM

 
Blogger f(n) said...

Karl,
Yes, using == instead of strncmp doesn't work. The constants are in different locations (I believe the runtime is shared).
Also, ([thing count] == 0) and (![thing count]) are optimized to exactly the same thing. Not only are they each 13 instructions, but they are the exact same instructions. So no dice on that one, either. And I find "== 0" more readable, especially with the brackets and parentheses. It is to easy for the "!" to get overlooked.

October 10, 2005 6:16 PM

 
Anonymous Karl Adam said...

Nathan, I was suggesting you use == to compare the obj_class objects since those are shared in the runtime. There should only be once instance per class, so merely comparing them by pointer should be much faster than the name comparison.

I'm not surprised the compiler optimizes ! var and var == 0 to the same thing really, just seemed likely that ! would remove at least one comparison from the overall comparison. So I guess ! is now just a style thing for me.

October 10, 2005 8:38 PM

 
Anonymous Karl Adam said...

Nathan, BTW This is what I meant:

if (thing == nil) return TRUE;

thingClass = (Class)thing->isa;

do {
switch (thingClass) {
case NSArrayClass :
return ([(NSArray *)thing count] == 0);
break;
case NSDataClass :
return ([(NSData *)thing length] == 0);
break;
case NSStringClass :
return ([(NSString *)thing isEqualToString:@""]);
break;
} while (thingClass = thingClass->super_class);

October 10, 2005 8:49 PM

 
Anonymous Anonymous said...

Harvey,

The idea of implementing a -count method on NSString is fine, except that I would expect it to always return 1.

Some time ago, I was writing an API where I wanted the same methods to deal with single objects as if they were arrays, so that I wouldn't have [NSArray arrayWithObject:] all over the place in my code.

I implemented -count, -objectEnumerator, and -reverseEnumerator methods in a category on NSObject, so any object could be treated as an array of one item.

October 11, 2005 2:33 AM

 
Blogger f(n) said...

Karl,
Again, case labels have to be evaluated at compile-time. Since my variables aren't known until I look at the runtime, they are ineligible to be used as case labels. So your suggested code doesn't work.
Second, my problem was that without using +class, I did not now what the class object was. All I knew was the location of an instance of a subclass and the name of the class I wanted. Based on that, how am I supposed to find the class object without comparing strings? And if I do know the location of the Class object, which was the whole point of that code, why would I bother running that code? As I demonstrated in my last iteration, +class is a much cleaner way to get the class object and makes the earlier code look silly, but given just those two pieces of information, the location of a subclass instance and the name of the class I want, I don't see a better way to write that code.

October 11, 2005 7:13 AM

 
Blogger Abhi Beckert said...

Sheesh, all this fuss over something so simple... How many hours did you guys spend working on these solutions? Why do something so damn complex?

This is what I'd do:

static inline BOOL IsEmpty(id thing) {
if ( thing == nil ) return NO;

return [thing isEmpty];
}

And then have a category for each class you want to be able to test. NSString would be:

- (BOOL)isEmpty
{
return [self isEqualToString:@""];
}

October 13, 2005 8:09 PM

 
Blogger Abhi Beckert said...

Ooops lol, replace return NO; with return YES;... :-/

October 13, 2005 8:09 PM

 
Anonymous Alex Rosenberg said...

Darn. I was about to post a solution much like Mirko's. The remaining problem is that __builtin_types_compatible_p was added to GCC for the non-Motorola AltiVec support and that case didn't need to check inheritance. File a Radar requesting that they add such checking.

October 18, 2005 7:06 PM

 

Post a Comment

<< Home