With few exceptions, using the C preprocessor is a code smell. C++ programmers have had this beat into them: “Don’t use the preprocessor to do something the language itself provides.” Unfortunately, more than a few Objective-C programmers have yet to get that message.
[This post is part of the Code Smells in Objective-C series.]
Here’s a handy command to run from Terminal. It examines source files from the current directory down, showing preprocessor use that you should double-check.
This command builds in some exceptions. For example, #import directives are vital. #pragma mark can be useful. …But I want to question pretty much everything else! Why does it matter? Because every time you use the preprocessor, what you see isn’t what you compile. And for #define macros used as constants, there are certain pitfalls to avoid — when we could just avoid them altogether.
Here are some common preprocessor idioms, and how to replace them:
- Constants: Numeric constants
- Constants: Ascending integer constants
- Constants: String constants
- Conditional compilation: Commenting out code
- Conditional compilation: Switching between experiments
- Conditional compilation: Switching between staging and production URLs
- Conditional compilation: Supporting multiple projects or platforms
Let’s start with a simple one that comes from our C heritage:
Unless you’re delivering platform-agnostic C or C++ code, there’s no reason to use #include, along with the accompanying include guards. Use #import instead; it eliminates the need for those #ifndef include guards.
Just because you’re in Objective-C doesn’t mean you can’t use plain C functions! Unless your macro relies on preprocessor magic like __LINE__, rewrite it as a standalone function. (And even then, have your macro call another function and shift as much as you can to the function.)
And this isn’t your dad’s C! The C language continues to adopt small pieces of C++. One of these is the ability to inline functions:
Now we begin a set of preprocessor smells around constants. Using constants instead of repeating literal values is commendable. Using #define to create constants is not.
If a constant is used only within a single file, make it a static const. We give it an explicit type that adds to its semantic meaning. For that matter, the numeric literal can be expressed more simply if you like, because the explicit type clarifies the acceptable domain of values. So here’s what we get instead:
If a constant is shared across files, make it available it the way you do with everything else: create a declaration in the header file, and a definition in one implementation file. (Of course, you’re following Apple’s coding guidelines and using prefixes on your names. Right?) So the .h file has the declaration:
And the .m file has the definition:
#define lastNameRow 1
#define address1Row 2
#define cityRow 3
Ascending integer constants are handy for coding table views, to determine which information falls on which cell. …That’s what enumerated types are for.
Enumerated types make it easy to rearrange the order or add new values. In general, people use #defines because it’s easier to construct a dangerous macro than a safe constant. But here’s a case where what the language provides is not only safer, but easier.
An enumerated type doesn’t have to be named. But if you pass any of these values as arguments, you’ll want to define a type name to increase compiler checks and add semantic meaning. Rather than having to write “enum Address” everywhere you want to use the “Address” enumerated type, it’s common to create a typedef like this:
As with numeric constants, use the language to define a constant. Only this time, we’re defining a constant string, which is really an object, which is expressed in Objective-C as a pointer. So we want to define a constant pointer.
Constant strings are often shared across multiple files, so here’s how to declare the constant in the .h file:
The definition in the .m file is then:
Conditional compilation, in its various flavors (#if, #ifdef, etc.), is a way to selectively enable or disable chunks of code. It’s used for different purposes, but it’s always a blunt instrument.
In the old days of C, the only form of commenting was /* … */. To comment out a chunk of code, you’d add /* in front and */ at the end. Then someone discovered that this didn’t work if the code already contained a comment. What to do? The answer at the time was to use the preprocessor: wrapping the code in #if 0 did the trick.
But that was a long time ago, before the dawn of modern IDEs and their color-coded ways. The color-coding helps us visually parse code more easily… but not for this. Even though there’s a 0 in this case, in general the IDE can’t know whether to show that conditional compilation has removed a chunk of code in a source file. So there’s no visual indicator that the code is commented out! It looks just like the rest of the code.
Fast-forward both C and Xcode to the present day. C has continued to evolve, and adopted the // commenting style from C++. Xcode takes advantage of this, and offers a “Comment Selection” command in the menus. Just press ⌘/ to comment out a section of code: Xcode will add // to the beginning of each selected line, color-coding them as comments. Pressing ⌘/ again reverses the process, bringing the code back.
So Xcode makes it easy to enable and disable code. But there’s another problem that we’ll get into in the following section: commenting-out code is fine if it’s temporary and you plan to clean it up soon. But too often, it’s just left there to rot…
Sometimes, you’re coding experimentally. Or you want a quick way to switch back-and-forth between two approaches. That’s fine.
But at some point, a decision is made. The experimental approach is validated, and you’re ready to ship. Clean up after yourself! Unless there’s some important historical reason to keep the rejected code as a comment, delete it. And if you choose to keep it, get rid of the preprocessor directives. Turn it into a real comment with explanation, not just code.
static NSString *const fooURLString = @"https://dev.foo.com/services/fooservice";
static NSString *const fooURLString = @"https://foo.com/services/fooservice";
When you develop service-based applications, you want to be able to specify whether you’re talking to the real production service, or to a staging service.
For simple apps with few URLs, I create a class for the URLs, and access them through methods:
DebugSettings *debugSettings = [self debugSettings];
if ([debugSettings usingStaging])
For complicated apps that talk to many services, consider putting URLs into a plist instead. See Switching from Staging URL to Production URL for a plist example.
When you have code that’s shared across multiple projects (or multiple platforms), it’s tempting to sneak in project-specific extensions into a shared source file. It may seem expedient, but it pollutes the source and conceals opportunities for unifying the code.
We work in an object-oriented language, so let’s use OO patterns, shall we? The basic strategy is to rework the methods containing project-specific code into Template Methods, with project-specific operations provided by project-specific subclasses.
- Create a subclass for each project variant.
- In each project, add the subclass for that project.
- Compile each project.
- Create a factory method that uses #if to create the right subclass. (We’re introducing one use of the preprocessor so that we can eliminate the others.)
- Find each place that instantiates the original class. Have it call the factory method instead.
- Compile and test each project.
- For each conditionally compiled section:
- Perform Extract Method to determine the required signature.
- Move each platform-specific section of the body down to the platform-specific subclass, until the method at the base class is empty.
- Compile and test each project.
- Look for Duplicated Code within each subclass, and across the subclasses.
If you end up with multiple platform-specific subclass hierarchies across your code, you may find opportunities to use the Bridge pattern.
Avoid the C preprocessor!
Again, execute this command in Terminal to find the potentially offending preprocessor directives in your code. How many do you find? Can you reduce them? Are the ones that remain justified?
Remember: Don’t use the preprocessor to do something the language itself provides!
Question: Where do you still find the preprocessor helpful? What are the alternatives? Leave a comment below.