C Switch for Non-Integers

Jared Pochtar
4 min readSep 6, 2010

--

In C, and thus Objective-C, a switch statement only works with integers, and the case labels all need to be constant. Strings seem like a natural thing to switch on, especially for command line interfaces. Unfortunately, they cannot be reduced to integer constants, so we cannot use them. Now that we have blocks in the language, we can create a dictionary where we associate any object, such as an NSString, with a block, and call the selected block after a simple dictionary lookup.

The syntax

//get a string from standard input
char str[256];
fgets(str, 256, stdin);
*(char *)memchr(str, '\n', 256) = '';
JPSwitch([NSString stringWithUTF8String:str]) {
JPCase(@"option one"):
NSLog(@"the first posibility was selected");

JPStringCase(Do Non-NSString objects work?):
NSLog(@"Yes, technically any object would work as a key,"
@"and anything else would work with an NSValue wrapper");
JPDefaultCase:
NSLog(@"None of the possibilities were selected. Oh noes!");
} JPSwitchEnd;

Due to the unordered nature of a dictionary, there is no (simple) way to enable fall-through, so control-flow breaks immediately upon a subsequent case statement, eliminating the need for the break keyword. More than that however, I view the lack of a break keyword as a net positive because they usually simply require extra typing, creating a reduction of code density, compared to the relative few of times fall-through functionality is necessary.

The JPSwitchEnd macro and its exact usage is something I agonized for a while over, as it would be much more C-like if the switch statement ended at the closing brace. The main point of interest was the semicolon's inclusion or exclusion from the macro, but ultimately I decided on its exclusion so that the JPSwitch statement would look closer to the C do...while statement, instead of a dangling reminder of an ugly macro.

The Macro Hackery

Peeling back the layers, here is what the JPSwitch statement above would look without macros:

__JPSwitch([NSString stringWithUTF8String:str], ^{
}, @"option one", ^{ JPSwitchCase:
NSLog(@"the first posibility was selected");
}, @"Do Non-NSString objects work?", ^{ JPSwitchCase:
NSLog(@"Yes, technically any object would work as a key,"
@"and anything else would work with an NSValue wrapper");
}, &JPSwitchDefaultCaseIndicator, ^{ JPSwitchCase:
NSLog(@"None of the possiblities were selected. Oh noes!");
} , &JPSwitchTermationIndicator);

as you can see, the macros just barely cover a variadic function, which does the real work. Before we delve into that function, a few things to note which may be non-obvious:

The empty block that is the second parameter to __JPSwitch is there so that the first case ("option one") will not have any errors upon the closing brace that it begins with. Because we want the control flow of the previous case to break upon the following case, it is vital that we include this all on one line, hence in one macro, hence the closing brace (for all other cases).

Another thing to note is the JPSwitchCase label which precedes every case; this is so we can have the colon after the (macros) of the case labels, but serves no actual purpose. They do not conflict with each other because they are in separate blocks.

Lastly, note the &JPSwitchDefaultCaseIndicator and &JPSwitchTerminationIndicator. Variadic functions need a sentinel value to signal the end of their parameter list, but typically NULL (which is the same as nil) is the colloquial sentinel for pointer types. Because methods often return nil when unable to return a valid object, it is possible that a case label object may be nil. (In order to make __JPSwitch nil safe, we will ignore nil case labels and their blocks.) The reason the NULL constant is used, however, is because it is guaranteed that no actual object (in the C sense of the word) can occupy the space pointed to by NULL, and therefore it can never accidentally equal a valid pointer. With this in mind, it is easy to create a pointer sentinel value: just allocate global dummy memory and use its pointer, that way no valid object's pointer can compare equal to it. Thinking myself rather clever, I used this approach for the default case label as well.

The result://JPSwitch.h
extern char JPSwitchTermationIndicator;
extern char JPSwitchDefaultCaseIndicator;
void __JPSwitch(id selector, ...);
#define JPSwitch(selector) __JPSwitch(selector, ^
#define JPCase(obj) }, obj, ^{ JPSwitchCase
#define JPStringCase(str) JPCase(@#str) //Preprocessor stringification
#define JPDefaultCase }, &JPSwitchDefaultCaseIndicator, ^{ JPSwitchCase
#define JPSwitchEnd , &JPSwitchTermationIndicator)
__JPSwitch

The majority of its workings having been exposed already, there is one last intricacy of __JPSwitch. I mentioned above that it used a dictionary; it does not, however, use an NSDictionary. While it could be using a C++ std::map, I chose to keep this purely Objective-C, although if I hadn't I would be making use of the templating in std::map for the case label object, making it compatible with any object, C, Objective-C, or C++. Instead, I used NSMapTable. The reason I chose it over NSDictionary is that NSMapTable is more than happy to accept arbitrary pointers, but more importantly not retain them. This is significant because when you retain a block, you copy it, which can be expensive. This way, you have very little overhead for unused code. While the rest should be left as an exercise for the reader, it's on my github, so I'll post it here as well:

//JPSwitch.m
#import "JPSwitch.h"
char JPSwitchTermationIndicator, JPSwitchDefaultCaseIndicator;void __JPSwitch(id selector, ...) {
va_list args;
va_start(args, selector);
id caseLabel;
void(^caseBlock)();
void(^defaultCaseBlock)() = nil;
va_arg(args, void(^)()); //Eat empty first blockNSMapTable *allCases = [NSMapTable mapTableWithWeakToWeakObjects];
while ((caseLabel = va_arg(args, id)) != (void *)&JPSwitchTermationIndicator) {
caseBlock = va_arg(args, id);
if (caseLabel == (void *)&JPSwitchDefaultCaseIndicator) {
defaultCaseBlock = caseBlock;
} else if (caseLabel != nil) {
[allCases setObject:caseBlock forKey:caseLabel];
}
}
caseBlock = (void(^)())[allCases objectForKey:selector] ?: defaultCaseBlock;
if (caseBlock) caseBlock();
}

Conclusion

So now you can use a switch statement over any Objective-C object, including NSStrings. Additionally, you can switch with anything else by wrapping it in an NSValue. I think this could be best used in parsing and especially in command line interfaces.

You can download the entire project here.

--

--