Constants in objective-c, a macro that keeps you DRY

Written by: Eyal Keren

7 min read

Stay connected

"There aren’t any small programming problems only small programmers", I don’t know if this sentence is even true just wanted to have a grand opening to my blog post, which is covering what is usually considered a small problem: how do you define your constants. Lets examine this code:

if ([key isEqualToString: @“myJsonKey”])
{
// Do something, probably with value
}

First, I’ll surprise you, I don’t see anything wrong with this if statement. It will definitely pass my code review although none of my colleagues agree it should. Many programmers think that all constant should be delivered with some level of abstraction, I think that the above code is succinct, it is self explanatory and doesn’t require the reader to jump to another place to figure out what exactly happens. But it has some problems, because, when code base gets successful it usually grows... This @“myJsonKey”  will eventually be required elsewhere and then the temptation of the programmer is to just do the same thus causing code duplication that will eventually introduce bugs to the system. So lets assume we use this constant elsewhere and even I agree that some level of indirection is required here, and lets commit this code:

#import “MyConstants.h”
// …
if ([key isEqualToString: MyJsonKey])
{
    // Do something, probably with value
}

Now this code, on the contrary, will not pass my code review. it is not that a the programmer did a shity job, he picked the wrong language to do it in. Objective-C doesn't have any namespace mechanism so right now it seems like the global namespace was polluted with the symbol MyJsonKey. This constant declaration does not scale between other developers in the same group and third party code that is running without namespace also. And if you are in the business of writing your own SDK you are probably going to figure out that you did something wrong when a customer gets the duplicate symbol  error. So what do you do when there is no native support for namespaces? You concat strings. Lets fix this code:

#import “MyConstants.h”
// …
if ([key isEqualToString: MyNamespace_JsonKeys_MyJsonKey])
{
    // Do something, probably with value
}

  Now I am settled, lets just click on the MyNamespace_JsonKeys_MyJsonKey  in AppCode (or Xcode) and let's see how it is defined. Option 1: (Macro)

// “MyConstants.h” //

#define MyNamespace_JsonKeys_MyJsonKey @“myJsonKey"

Simply define a new macro for substitution in the header file (MyConstats.h ). Is it bad? Yes. But like most things in programming the fact that it is a bad solution doesn’t mean you will not see it anywhere and use it yourself in many cases. It is important to find out what's going on here and why this is a bad decision. The #include or #import preprocessing directive just scans the content of the input file before it continues to the next line, so in our example:

#import “MyConstants.h”
// …
if ([key isEqualToString: MyNamespace_JsonKeys_MyJsonKey])
{
    // Do something, probably with value
}

Turns into:

#define MyNamespace_JsonKeys_MyJsonKey @“myJsonKey"
// …

if ([key isEqualToString: MyNamespace_JsonKeys_MyJsonKey])
{
    // Do something, probably with value
}

And when the preprocessor gets to the MyNamespace_JsonKeys_MyJsonKey token it just replaces it with the actual value defined by the #define  macro directive, so basically, our code ends with this:

if ([key isEqualToString: @“myJsonKey"])
{
    // Do something, probably with value
}

Looks familiar? It should be if you are paying attention. You can see this output by using clang’s -E flag  clang -E myObject.m If you want to take even a deeper look and extract the implementation that lies beneath objective-c @  literal, we are actually running this code:

if ([key isEqualToString: [NSString stringWithUTF8String: “myJsonKey"])
{
    // Do something, probably with value
}

So, by using the macro substitution we eliminated code duplication for the developer but the actual compilation unit (which is constructed long after macros are substituted) is constructing the same value multiple times. Do we even care? Let's see what the implications of this issue are: Consider this code:

[MyNamespace_JsonKeys_MyJsonKey isEqual:MyNamespace_JsonKeys_MyJsonKey]

It is actually the same like writing: (would love to hear comments regarding known optimizations that LLVM does here?)

[[NSString stringWithUTF8String: “myJsonKey”] isEqual: [NSString stringWithUTF8String: “myJsonKey”]]

So Basically:

  1. We create a lot more objects then we should, allocating space in the heap and deallocating it every time we use this constant

  2. The comparison of an object usually start with the optimistic question: if(self==other) return YES; In this case it will fail because the objects are different so it will eventually need to call the  isEqualToString functions that checks equality by iterating over all the characters. kinda costly… sometimes important (but beware of premature optimizations)

  3. Compile time, or better yet, which files need to recompile when I change the header. Because I actually change the header file, all the .m files that depend on it need to recompile

Well, basically this is all very nice but not really true, the LLVM folks are smarter than this and they already optimized the creation of NSString with the @  literal, eventually these calls do not generate excessive objects and the same exact object is returned for each call. Also, LLVM doesn't really uses  stringWithUTF8String to generate these objects, it uses a different mechanism (NSConstantString). If you look deeper into LLVM intermediate code  generated by the clang -Os -S -emit-llvm <file.m> command, you'll see how this code actually gets translated. But this is for a different talk... We can debate whether these are real issues or not. It can be expensive if used inside some iterating code, or it can be an issue depending on the constraints your system has (realtime/memory constraint), but it is much more important to first understand what we actually coded, and did we mean it to work this way. Option 2: (declare in header, define in code)

// “MyConstants.h”  //

extern const NSString* MyNamespace_JsonKeys_MyJsonKey;// better to use FOUNDATION_EXPORT instead of extern
// “MyConstants.m”  //

NSString* const MyNamespace_JsonKeys_MyJsonKey = @“myJsonKey”;

If we want to practice the preprocessor work here, this is the result after the preprocessor finishes his job:

#import “MyConstants.h”
// …
if ([key isEqualToString: MyNamespace_JsonKeys_MyJsonKey])
{
    // Do something, probably with the value
}

Turns into:

extern NSString * MyNamespace_JsonKeys_MyJsonKey ;
//...
if ([key isEqualToString: MyNamespace_JsonKeys_MyJsonKey])
{
    // Do something, probably with the value
}

So who is responsible for generating the actual value? Well, MyConstants.m will be compiled into a MyConstants.o and the linker will link this MyNamespace_JsonKeys_MyJsonKey reference to the instance that was created in the .o file So in this example, if we consider this code again:

[MyNamespace_JsonKeys_MyJsonKey isEqual:MyNamespace_JsonKeys_MyJsonKey]

It  remains the same code, nothing get macro substitute in the way it doesn't create any objects in the process and optimistically verifies that self == other in isEqual implementation But the ugly truth needs to be told, we need to define a constant in two different places. The only premature optimisation I love is optimising the time of development, and this .h .m files is a don't repeat yourself nightmare. We want to achieve the previous runtime (compiled sources) behaviour without the need to repeat ourselves and write every constant in both the header and the .m file. So let's harness to the power of MACRO substitution with a small trick that will make the macro to be read differently when it is used by MyConstants.m The trick is pretty simple, we write the macro to behave differently when it is called from MyConstats.m. This is how MyConstats.m looks like: Option 3: (a DRY Double declaration using a macro magic trick)

// MyConstants.m //
#define MyConstats_importingFromDotM // This will tell MyConstant.h to use the definition macro
#import "MyConstats.h"

So when MyConstats.h  is read from the #import  of myContats.m  it will have the MyConstats_importingFromDotM  MACRO defined Now lets look at MyConstats.h  (for simplicity I'll show you only NSString* constants here)

#ifdef MyConstats_importingFromDotM

#define MyNameSpace_NSString_CONST(key, value) NSString* const key = value;
#else
#define MyNameSpace_NSString_CONST(key, value) FOUNDATION_EXPORT NSString* key;
#endif

MyNameSpace_NSString_CONST(MyNamespace_JsonKeys_MyJsonKey, @"MyJsonKey")

So the definition line MyNameSpace_NSString_CONST(MyNamespace_JsonKeys_MyJsonKey, @"MyJsonKey") is viewed differently by different files All the files will see it as:

FOUNDATION_EXPORT NSString* MyNamespace_JsonKeys_MyJsonKey;

and MyConstants.m will see it as

NSString* const MyNamespace_JsonKeys_MyJsonKey = @”MyJsonKey”;

Same as option 2 without the need to write things twice. It still isn’t perfect because when changing a value it requires compilation of all the dependent files (although nothing is really changed for them) and here at rollout.io we invested a bit more work to solve the namespace issue, I’ll show you more in the next post… By the way, don’t forget to only declare immutable types as your constants.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.

Loading form...
Your ad blocker may be blocking functionality on this page. Please disable for an improved experience.