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?
Hard to maintain. Better use interfaces to abstract platform specific code than abusing conditional compilation by scattering #ifdef
s 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.
- Makefiles are used here illustratively. Your build system may not use
make
at all, such as if you use Visual Studio, CMake, Scons, etc. - Even if
common_stuff()
actually has multiple implementations, varying per platform.
bar.c
files. –
Overfly 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 (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 :(
#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 #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 #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 Because then when you do search results you don't know if the code is in or out without reading it.
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
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.
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
#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 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.
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.
#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 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.
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
.
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.
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.
© 2022 - 2024 — McMap. All rights reserved.