tags:

views:

1580

answers:

10

A programmer I respect said that in C code, #if and #ifdef should be avoided at all costs, except possibly in header files. Why would it be considered bad programming practice to use #ifdef in a .c file?

+29  A: 

Hard to maintain. Better use interfaces to abstract platform specific code than abusing conditional compilation by scattering #ifdefs all over your implementation.

E.g.

void foo() {
#ifdef WIN32
   // do Windows stuff
#else
   // do Posix stuff
#endif
   // do general stuff
}

Is not nice. Instead have files foo_w32.c and foo_psx.c with

foo_w32.c:

void foo() {
    // windows implementation
}

foo_psx.c:

void foo() {
    // posix implementation
}

foo.h:

void foo();  // common interface

Then have 2 makefiles1: Makefile.win, Makefile.psx, with each compiling the appropriate .c file and linking against the right object.

Minor amendment:

If foo()'s implementation depends on some code that appears in all platforms, E.g. common_stuff()2, simply call that in your foo() implementations.

E.g.

common.h:

void common_stuff();  // May be implemented in common.c, or maybe has multiple
                      // implementations in common_{A, B, ...} for platforms
                      // { A, B, ... }. Irrelevant.

foo_{w32, psx}.c:

void foo()  {  // Win32/Posix implementation
   // Stuff
   ...
   if (bar) {
     common_stuff();
   }
}

While you may be repeating a function call to common_stuff(), you can't parameterize your definition of foo() per platform unless it follows a very specific pattern. Generally, platform differences require completely different implementations and don't follow such patterns.


  1. Makefiles are used here illustratively. Your build system may not use make at all, such as if you use Visual Studio, CMake, Scons, etc.
  2. Even if common_stuff() actually has multiple implementations, varying per platform.
Alex
+1, but I'd love an example :-)
Norman Ramsey
There's no need for multiple makefiles. That's what configure scripts are for.
William Pursell
Well, depends on the build system. You may not have any makefiles, such as if you use Visual Studio, or CMAKE, or Scons, or any other tool. My example was illustrative.
Alex
Doesn't this go against DRY?
Pod
What happened with the "do general stuff"? Is it copy and pasted in both files?
quinmars
Common code need not be separated in platform specific files like shown above. All platform independent code goes in general `bar.c` files.
Alex
no i don't like this! :( my experience has shown that where possible you should put all platform independant stuff in the source files, you want to make it as invisible and seamless as possible. header clutter, and create platform dependent makefiles are terrible to maintain.
Matt Joiner
@Pod: I've found in production code that the large majority is platform-independent and it's only small pieces that go in the `foo_{w32,psx,...}.c` files. While yes, there's often more repetition in the platform-specific bits than you might like, DRY does encourage refactoring to minimize.
Donal Fellows
I find it astonishing that the difficulty of maintaining #ifdefs is considered an advertisement for the above...
Edmund
+3  A: 
  1. Because then when you do search results you don't know if the code is in or out without reading it.

  2. Because they should be used for OS/Platform dependencies, and therefore that kind of code should be in files like io_win.c or io_macos.c

Simeon Pilgrim
+2  A: 

A reasonable goal but not so great as a strict rule

The advice to try and keep preprocessor conditionals in header files is good, as it allows you to select interfaces conditionally but not litter the code with confusing and ugly preprocessor logic.

However, there is lots and lots and lots of code that looks like the made-up example below, and I don't think there is a clearly better alternative. I think you have cited a reasonable guideline but not a great gold-tablet-commandment.

#if defined(SOME_IOCTL)
   case SOME_IOCTL:
   ...
#endif
#if defined(SOME_OTHER_IOCTL)
   case SOME_OTHER_IOCTL:
   ...
#endif
#if defined(YET_ANOTHER_IOCTL)
   case YET_ANOTHER_IOCTL:
   ...
#endif
DigitalRoss
Donal Fellows
A: 

If your code will be compiled with different C compilers, and you use compiler-specific features, then you may need to determine which predefined macros are available.

Chip Uni
Then you should do that in a header file you control, to abstract away the differences. Or if it's a difference of inline assmebly that should be in different code files...
Simeon Pilgrim
+1  A: 

CPP is a separate (non-Turing-complete) macro language on top of (usually) C or C++. As such, it's easy to get mixed up between it and the base language, if you're not careful. That's the usual argument against macros instead of e.g. c++ templates, anyway. But #ifdef? Just go try to read someone else's code you've never seen before that has a bunch of ifdefs.

e.g. try reading these Reed-Solomon multiply-a-block-by-a-constant-Galois-value functions: http://parchive.cvs.sourceforge.net/viewvc/parchive/par2-cmdline/reedsolomon.cpp?revision=1.3&view=markup

If you didn't have the following hint, it will take you a minute to figure out what's going on: There are two versions: one simple, and one with a pre-computed lookup table (LONGMULTIPLY). Even so, have fun tracing the #if BYTE_ORDER == __LITTLE_ENDIAN. I found it a lot easier to read when I rewrote that bit to use a le16_to_cpu function, (whose definition was inside #if clauses) inspired by Linux's byteorder.h stuff.

If you need different low-level behaviour depending on the build, try to encapsulate that in low-level functions that provide consistent behaviour everywhere, instead of putting #if stuff right inside your larger functions.

Peter Cordes
Ok, just nitpicking here, but im not sure about CPP not being Turing-complete. Consider http://en.wikipedia.org/wiki/Lambda_calculus via conditional inclusion.
David X
The non-Turing-Complete comment was just restating what I read <a href=http://en.wikipedia.org/wiki/Assembly_language#Macros>on wiki</a>. The claimed reason there is lack of loop or even goto constructs. Does the lambda calculus require recursion for Turing completeness? Because CPP macros can't be recursive. You can't have conditional expansion based on the parameter (can you?), so you couldn't define a terminating condition. I have to admit to not knowing much of Lambda calculus beyond having heard of it, and wondering if knowing it would help me grok emacs. :/
Peter Cordes
sorry about the formatting; that was supposed to be two paragraphs. I didn't realize comment fields were so different from posts. The link is http://en.wikipedia.org/wiki/Assembly_language#Macros
Peter Cordes
Actually, having tried it, it looks like you're right. Variable reassign doesn't work (eg: `#define X (X+1)`) and everythings in one namespace so no `with x = (x+1):{...}`, so I cant find a way to pass varying arguments to recursive functions (eg: `fact(x) = (x*fact(x-1))`). CPP macros can be recursive, though (`#include __FILE__`), just not usefully so.
David X
+2  A: 

My interpretation of this rule: Your (algorithmic) program logic should not be influenced by preprocessor defines. The functioning of your code should always be concise. Any other form of logic (platform, debug) should be abstractable in header files.

This is more a guideline than a strict rule, IMHO. But I agree that c-syntax based solutions are preferred over preprocessor magic.

catchmeifyoutry
+4  A: 

(Somewhat off the asked question)

I saw a tip once suggesting the use of #if(n)def/#endif blocks for use in debugging/isolating code instead of commenting.

It was suggested to help avoid situations in which the section to be commented already had documentation comments and a solution like the following would have to be implemented:

/* <-- begin debug cmnt   if (condition) /* comment */
/* <-- restart debug cmnt {
                              ....
                          }
*/ <-- end debug cmnt

Instead, this would be:

#ifdef IS_DEBUGGED_SECTION_X

                if (condition) /* comment */
                {
                    ....
                }
#endif

Seemed like a neat idea to me. Wish I could remember the source so I could link it :(

paul
I just use `#if 0 ... #else ... #endif` to temporarily change code for debugging. Then change the 0 to a 1 to go back to the original code.
Steve Jessop
Oh, and try to remember not to check it in ;-)
Steve Jessop
Ah, good call on the `#if 0`. I have used an ifdef/else like that to keep a nice unblemished copy of the original code, but hadn't thought to enable toggling like that.(my initial example above was only intended to show straight removal of code)I shall have to remember the toggle. Thanks :)
paul
A: 

By all means, favor abstraction over conditional compilation. As anyone who has written portable software can tell you, however, the number of environmental permutations is staggering. Some design discipline can help, but sometimes the choice is between elegance and meeting a schedule. In such cases, a compromise might be necessary.

Steve Emmerson
+1  A: 

The conditional compilation is hard to debug. One has to know all the settings in order to figure out which block of code the program will execute.

I once spent a week debugging a multi-threaded application that used conditional compilation. The problem was that the identifier was not spelled the same. One module used #if FEATURE_1 while the problem area used #if FEATURE1 (Notice the underscore).

I a big proponent of letting the makefile handle the configuration by including the correct libraries or objects. Makes to code more readable. Also, the majority of the code becomes configuration independent and only a few files are configuration dependent.

Thomas Matthews
I couldn't agree more. I've fixed too many conditionally compiled bugs that took days when they should have been resolved in minutes.
s1n
A: 

It's true that #if #endif does complicate the reading of the code. However I have seen a lot of real world code that have no issues using this and are still going strong. So there may be better ways to avoid using #if #endif but using them is not that bad if proper care is taken.

Kevin