#if vs #ifdef
feat. #elif & defined
I've always been interested in cross-platform development for some time now and so I'm trying to tackle it head first. This means tough times ahead and many hours will be spent scouring obscure posts and forums, but hopefully I'll be able to come out of it having improved as a programmer and obtaining some valuable skills.
One of the necessary things I've had to use are #ifdef statements to separate platform specific code and this led to me to investigate a little further about how they work. The basic way that they work is that if the condition is met, the compiler will compile the code inside the statement. Otherwise, it will ignore the code within the statement as if it weren't there. This is very handy when you want to call a function specific to Windows, but not on OS X or Linux. You can also do things like have debug print statements exist only in debug mode, without having to comment them in and out to toggle them on and off.
One of the most common usages is the #ifdef statment, which checks if a symbol is defined. I've been doing a little research on other commands such as #ifndef, #if, and so on and started learning the little nuances between them. Here are some of the things I've found.
Let's say I'm using Visual Studio on Windows and I've already made the preprocessor define the symbol 'WINDOWS'in the project settings.
#ifdef WINDOWS
//This code compiles just fine if WINDOWS is defined
DoWindowsStuff();
#endif
#if WINDOWS
//This will work as well
DoWindowsStuff();
#endif
Pretty simple and it works. You could use #ifdef or plain #if in this situation, but there is a difference between the two! Let's define the symbol 'WINDOWS' ourselves:
#define WINDOWS
#ifdef WINDOWS
//Works just fine
DoWindowsStuff();
#endif
#if WINDOWS
//Error: expected an expression
DoWindowsStuff();
#endif
Uh-oh. That didn't work and the compiler is complaining. So what changed? In Visual Studio, when you set a preprocessor value, it will automatically do something like:
#define WINDOWS 1
Where the symbol is assigned a value of 1. Let's do the same for ours.
#define WINDOWS 1
#ifdef WINDOWS
//Works just fine
DoWindowsStuff();
#endif
#if WINDOWS
//Now it works
DoWindowsStuff();
#endif
Now let's try another value, like 0
#define WINDOWS 0
#ifdef WINDOWS
//Works just fine
DoWindowsStuff();
#endif
#if WINDOWS
//Is ignored and does not compile!
DoWindowsStuff();
#endif
So what is the difference? The #ifdef will check if the symbol exists,whereas #if checks the value [1].
So is there a potential pitfall for this? Perhaps. (Although I'm not sure how many people define a value to 0 and use #if).
Let's take another example:
#define LINUX 1
#ifdef WINDOWS
DoWindowsStuff();
#elif LINUX
//Linux must be defined and and has a value that hopefully isn't 0...
DoLinuxStuff();
#endif
It will work for the most part and most likely won't run into any issues, although it isn't entirely fool proof. Luckily for those of us who are picky, we have the 'defined' keyword that we can use. So let's modify the last statement to:
#define LINUX
#ifdef WINDOWS
//Ok
DoWindowsStuff();
#elif defined LINUX
//Ok, only checks if LINUX is simply defined
DoLinuxStuff();
#endif
It is a bit more wordy, although there is some flexibility that comes with using the 'defined' keyword [2]. You can do stuff like:
#if defined WINDOWS && defined MAC
ClosedFunction();
#elif defined LINUX || defined STEAMOS
OpenFunction();
#endif
Something I've read as good practice from the Steam Dev Days from Ryan C. Gordon (Icculus) is to include an else statement [3]. This is useful in the case you are developing for another platform but forget to call a specific function that each platform has to call, like say creating an OpenGL context or DirectX device.
#if defined WINDOWS
//Windows
#elif defined MAC_OS_X
//Mac OS X
#elif defined LINUX
//Linux
#elif defined APPLE_IOS
//iOS
#else
#error New unsupported platform detected!
#endif
This way, if you move on to a new platform, the compiler will tell you all the spots where you have platform dependent code or where you have yet to integrate dependent areas for that particular platform. For instance, in my own engine, printf works fine in terms logging output to a console for most platforms, except for android. In my code, I have:
void log_message( ... )
{
#if defined WINDOWS || defined MAC_OS_X || defined LINUX || defined APPLE_IOS
printf( ...);
#elif defined ANDROID
__android_log_print(...);
#else
#error Console logging not integrated on this platform
#endif
}
This made it useful to separate out a version specific to android, and now if I develop on to another platform, the compiler will tell me I have yet to test message logging on it. It also means I now know that can't assume printf will just output to a console on some platforms.
And there you have it. Hopefully this will help you out in cross platform development!
References:
[1] http://stackoverflow.com/questions/3802988/difference-between-preprocessor-directives-if-and-ifdef
[2] http://stackoverflow.com/questions/1714245/difference-between-if-definedwin32-and-ifdefwin32
[3] Gordon, "Getting Started with Linux Game Development",
http://media.steampowered.com/apps/steamdevdays/slides/gettingstartedwithlinux.pdf
[4] http://www.tru64unix.compaq.com/docs/base_doc/DOCUMENTATION/V40F_HTML/AQTLTBTE/DOCU_078.HTM
