The "ifdef way" is like :
code :
#ifdef STUFF
.. stuff a ..
#else
.. stuff b ..
#endif
command line :
compiler -DSTUFF %*
or
compiler %*
Whereas the "if way" is :
code :
#ifndef STUFF
// stuff not set
// could #error here
// or #define STUFF to 0 or 1
#endif
#if STUFF
.. stuff a ..
#else
.. stuff b ..
#endif
command line :
compiler -DSTUFF=1 %*
or
compiler -DSTUFF=0 %*
Why is the "if way" so much better ?
1. You can tell if the user set STUFF or not. In the ifdef way, not setting it is one of the boolean values, so you can't tell if the user made any intentional selection or not. Sometimes you want to ensure that something was selected explicitly because it's too dangerous to fall back to a default automatically.
2. You can easily change the default value when STUFF is not set. You can just do #ifndef STUFF #define STUFF 0 or #ifndef STUFF #define STUFF 1. To change the default with the ifdef way, you have to change the sense of the boolean (eg. instead of STUFF use NOTSTUFF) and then all your builds break because they are setting STUFF intead of NOTSTUFF (and that breakage is totally fragile and non-detectable because of point #1).
3. There's no way to positively say "not STUFF" in the ifdef way. The way not stuff is set is by not passing anything ot the command line, but frequently it's hard to track down exactly how the command line is being set through the convoluted machinations of the IDE or make system. If some other bad part of the build script has put a -DSTUFF on your command line, you can't easily undo that by just tacking something else on the end of the command line.
I think it's incontrovertible that the "if way" is just massively better, and everyone should use it all the time, and never use ifdef. And yet I myself still use ifdef frequently. I'm not really sure why, I think it's just because I grew up using ifdef for toggles, and I'm so used to seeing it in other people's code that it just comes out of my fingers naturally.
Anyway, I was thinking about this because I had some problems with some #defines at RAD, and I chased down the problem and cleaned it up, and it seemed to me that it was a pretty good example of "cbloom style robustination". I've never met anyone who writes code quite like me (some are thankful for that, I know); I try to write code that is hard to use wrong (but without adding crazy complexity or overhead the way Herb Sutter style code does).
(disclaimer : this is not intended as a passive aggressive back-handed way of calling out some RAD coder; the RAD code in question is totally standard style that you would see anywhere, and it wasn't broken, just hard for me to use)
Anyhoo, the code in question set up the function exporting for Oodle.h ; it was controlled by two #defines :
#ifdef MAKEDLL
#define expfunc __declspec(dllexport)
#else
#ifdef MAKEORIMPORTLIB
#define expfunc extern
#else
#define expfunc __declspec(dllimport)
#endif
#endif
Okay, so there are four usage cases :
1. building Oodle as a LIB - use -DMAKEORIMPORTLIB
2. building Oodle as a DLL - use -DMAKEDLL
3. building an app that uses Oodle as a LIB - use -DMAKEORIMPORTLIB
4. building an app that uses Oodle as a DLL - use no define
and that all works fine (*). But I found it hard to use; for example if I try to stick a -DMAKEXE on the command line and somebody already
set -DMAKEDLL, it doesn't do what I expected; and there's no way to definitely say "I want dllimport".
(* = actually it also works if you use -DMAKEORIMPORTLIB in case 4; specifying "dllimport" for functions is actually optional and only used by the compiler as an optimization)
So anyway here's the robustinated version :
#ifdef MAKEDLL
#define expfunc __declspec(dllexport)
#if defined(MAKELIB) || defined(IMPORTLIB) || defined(IMPORTDLL)
#error multiple MAKE or IMPORT defines
#endif
#elif defined(IMPORTDLL)
#define expfunc __declspec(dllimport)
#if defined(MAKELIB) || defined(MAKEDLL) || defined(IMPORTLIB)
#error multiple MAKE or IMPORT defines
#endif
#elif defined(MAKELIB)
#define expfunc extern
#if defined(MAKEDLL) || defined(IMPORTLIB) || defined(IMPORTDLL)
#error multiple MAKE or IMPORT defines
#endif
#elif defined(IMPORTLIB)
#define expfunc extern
#if defined(MAKELIB) || defined(MAKEDLL) || defined(IMPORTDLL)
#error multiple MAKE or IMPORT defines
#endif
#else
#error no Oodle usage define set
#endif
and usage is obvious because there's a specific define for each case :
1. building Oodle as a LIB - use -DMAKELIB
2. building Oodle as a DLL - use -DMAKEDLL
3. building an app that uses Oodle as a LIB - use -DIMPORTLIB
4. building an app that uses Oodle as a DLL - use -DIMPORTDLL
and it's much harder to use incorrectly, because you have to set one and only one. Also it's a little bit less
implementation tied, in the sense that the fact that MAKELIB and IMPORTLIB are actually the same thing is hidden
from the user in case that ever changes.
(and of course I instinctively used #ifdef for toggles when I wrote this instead of using #if)
I used to think that "robustinated" code was the One True Way to write code, and I wrote advocacy articles about it and tried to educate others and so on. I basically have given up on that because it's too frustrating and tiring trying to convince people about coding practices. And in my old age I'm more humble and no longer so sure that it is better (because the code becomes longer, and short to-the-point code has inherent advantages; also robustination takes coder time which could be spent on other things; lastly robustination also tends to make compiles slower which hurts rapid iteration).
But I do know it's the right way for *me* to write code. When I first came to RAD I tried very hard to write code the "RAD way" so that the style would be consistent and so on. That was a huge mistake, it was very painful for me and made me write very bad code and take much longer than I should have. Only after a few years in did I realize that to be productive I have to write code my way. In particular I need the code to be very strongly self-checking.