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.
"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."
ReplyDeleteYou actually can: this is what the -U option is for.
I completely agree though, #if is much better than #ifdef.
What's your opinion on overly short and generic macro names?
ReplyDeleteIf I had a penny every time I had to tangle out the mess when several library vendors thought it to be a Good Idea[TM] to use macros like MAKEDLL and STATICLIB.
I'd expect vendors and projects to have disambiguation warts, but that seems to be more an exception than the rule.
I've recently switched to #if as well, in the last couple of weeks. I've been programming C/C++ for... quite a long time. I don't know why it's taken me so long to figure out the benefits of #if. Good thing I never claimed to be the sharpest tool in the box...
ReplyDeleteTwo other advantages I've noted, on top of your list:
1. You can use Alt+G/Cmd-Option-J/M-./whatever to go to the place where the #define is defined, with all the usual working-on-other-people's-code type benefits. Doing this with a #ifdef only works if the define is actually defined...
(Compared to doing a find-in-files, this saves you anywhere from a bit of hassle to a lot of hassle, depending on how much the #define is used.)
2. You can perform arithmetic on the values to do a basic check that only one is defined. For example, "#if X+Y+Z!=1". It's a lot less typing than the #ifdef equivalent. (Unless I've been missing something all these years?) OK, so this isn't exactly foolproof, but I'm happy to use this if the check is right next to the list of mutually-exclusive defines, to trap simple errors.
Now just a question of training my fingers, which still like to follow up every "#if" with a quick "def"...
The biggest advantage of #if would be if #if warned you if the symbol was undefined, but I don't know of any compiler that does so.
ReplyDelete(This would be an advantage because it would catch typos when you misspelled the name in the #if.)
ReplyDelete@ Lars - what's my opinion? I think short generic names in the global namespace are great! Of course they're terrible. (our macros are actually things like OODLE_MAKEDLL, but I removed the prefixes for this blog)
ReplyDelete@ nothings - I always thought that an #if on an undefined symbol was a silent zero, but I have found at least one compiler that generates an error for it. So you can neither rely on it being a silent zero nor can you rely on it even being a warning (many compilers don't even generate a warning). The only robust multi-platform solution is to manually check for #ifdef before every #if , which is mildly annoying but at least it's quite clear in the code.
@ Tom - your point #1 reminds me of one of the big wins of the #if method (I'm basically just repeating and agreeing with what you wrote) -
ReplyDeletewhen you're in a strange codebase and you see some #ifdef that toggles a bunch of code, you are often left wondering - hmm is this actually defined? It would be nice to have a way to run the compiler (in the real production build script) and then port the results back to the code to show you what parts of the code are actually live. (in big spaghetti codebases this can be very hard to track down). (you'd also like to be able to port back info from the linker to mark up the code with all the parts that were dead-stripped)
With the #if method it's generally much easier, because you can grep for that symbol and hopefully find a clear place where it's set to 0 or 1.
@nothings: gcc (and clang) has -Wundef to give a warning for symbols that aren't defined. I don't know if there's anything similar for MSVC.
ReplyDeleteQuite by coincidence, I came across this just now: http://www.codersnotes.com/notes/easy-preprocessor-defines.
ReplyDeleteCan't quite decide what I think about it yet :) - but it looks like it should be fairly accident-proof.
insert squinty Fry "not sure if like" meme here.
ReplyDelete