Why should #ifdef be avoided in .c files?
Asked Answered
U

11

33

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?

Underworld answered 5/12, 2009 at 5:6 Comment(0)
O
59

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.
Overfly answered 5/12, 2009 at 5:8 Comment(7)
There's no need for multiple makefiles. That's what configure scripts are for.Probable
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.Overfly
What happened with the "do general stuff"? Is it copy and pasted in both files?Minotaur
Common code need not be separated in platform specific files like shown above. All platform independent code goes in general bar.c files.Overfly
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.Millepore
@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.Peril
I find it astonishing that the difficulty of maintaining #ifdefs is considered an advertisement for the above...Solfatara
G
6

(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 :(

Gaucherie answered 5/12, 2009 at 6:19 Comment(5)
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.Massive
Oh, and try to remember not to check it in ;-)Massive
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 :)Gaucherie
I thought #if 0 was a common idiom in C for temporarily disabling a section of code, since /* */ doesn't nest (the wording in this answer makes it seem like some secret trick).Craal
It was new to me, which might explain the wording -- I was self-taught with C, and haven't worked with it much professionally.Gaucherie
B
3
  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

Bloodsucker answered 5/12, 2009 at 5:10 Comment(0)
P
3

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.

Pycnidium answered 5/12, 2009 at 5:56 Comment(0)
D
2

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
Division answered 5/12, 2009 at 5:30 Comment(1)
There's also cases where you need #if defined(...) && !defined(...), such as when the OS defines two error codes or signals to be the same value. But it's advisable to corral such messiness in a single file if you can so that most of the time you don't have to pay it any attention...Peril
D
2

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.

Dapsang answered 5/12, 2009 at 20:23 Comment(1)
I couldn't agree more. I've fixed too many conditionally compiled bugs that took days when they should have been resolved in minutes.Vagary
G
1

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.

Glycerinate answered 5/12, 2009 at 5:56 Comment(4)
Ok, just nitpicking here, but im not sure about CPP not being Turing-complete. Consider en.wikipedia.org/wiki/Lambda_calculus via conditional inclusion.Argueta
The non-Turing-Complete comment was just restating what I read <a href=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. :/Glycerinate
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 en.wikipedia.org/wiki/Assembly_language#MacrosGlycerinate
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.Argueta
C
1

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.

Cuxhaven answered 5/12, 2009 at 16:36 Comment(0)
M
1

Consider the situation where you are required to provide fully tested code, with 100% branch coverage etc. Now add in conditional compilation.

Each unique symbol used to control conditional compilation doubles the number of code variants you need to test. So, one symbol - you have two variants. Two symbols, you now have four different ways to compile your code. And so on.

And this only applies for boolean tests such as #ifdef. You can easily imagine the problem if a test is of the form #if VARIABLE == SCALAR_VALUE_FROM_A_RANGE.

Marni answered 2/9, 2016 at 16:23 Comment(0)
B
0

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.

Barbule answered 5/12, 2009 at 5:46 Comment(1)
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...Bloodsucker
F
0

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.

Ferree answered 8/12, 2009 at 5:46 Comment(1)
"I have seen a lot of real world code" doesn't tell whyScriber

© 2022 - 2024 — McMap. All rights reserved.