Reputation: 4287
I have to use lot of #ifdef i386 and x86_64 for architecture specific code and some times #ifdef MAC or #ifdef WIN32... so on for platform specific code.
We have to keep the common code base and portable.
But we have to follow the guideline that use of #ifdef is strict no. I dont understand why?
As a extension to this question I would also like to understand when to use #ifdef ?
For example, dlopen() cannot open 32 bit binary while running from 64 bit process and vice versa. Thus its more architecture specific. Can we use #ifdef in such situation?
Upvotes: 19
Views: 16744
Reputation: 490408
With #ifdef
instead of writing portable code, you're still writing multiple pieces of platform-specific code. Unfortunately, in many (most?) cases, you quickly end up with a nearly impenetrable mixture of portable and platform-specific code.
You also frequently get #ifdef
being used for purposes other than portability (defining what "version" of the code to produce, such as what level of self-diagnostics will be included). Unfortunately, the two often interact, and get intertwined. For example, somebody porting some code to MacOS decides that it needs better error reporting, which he adds -- but makes it specific to MacOS. Later, somebody else decides that the better error reporting would be awfully useful on Windows, so he enables that code by automatically #define
ing MACOS if WIN32 is defined -- but then adds "just a couple more" #ifdef WIN32
to exclude some code that really is MacOS specific when Win32 is defined. Of course, we also add in the fact that MacOS is based on BSD Unix, so when MACOS is defined, it automatically defines BSD_44 as well -- but (again) turns around and excludes some BSD "stuff" when compiling for MacOS.
This quickly degenerates into code like the following example (taken from #ifdef Considered Harmful):
#ifdef SYSLOG
#ifdef BSD_42
openlog("nntpxfer", LOG_PID);
#else
openlog("nntpxfer", LOG_PID, SYSLOG);
#endif
#endif
#ifdef DBM
if (dbminit(HISTORY_FILE) < 0)
{
#ifdef SYSLOG
syslog(LOG_ERR,"couldn’t open history file: %m");
#else
perror("nntpxfer: couldn’t open history file");
#endif
exit(1);
}
#endif
#ifdef NDBM
if ((db = dbm_open(HISTORY_FILE, O_RDONLY, 0)) == NULL)
{
#ifdef SYSLOG
syslog(LOG_ERR,"couldn’t open history file: %m");
#else
perror("nntpxfer: couldn’t open history file");
#endif
exit(1);
}
#endif
if ((server = get_tcp_conn(argv[1],"nntp")) < 0)
{
#ifdef SYSLOG
syslog(LOG_ERR,"could not open socket: %m");
#else
perror("nntpxfer: could not open socket");
#endif
exit(1);
}
if ((rd_fp = fdopen(server,"r")) == (FILE *) 0){
#ifdef SYSLOG
syslog(LOG_ERR,"could not fdopen socket: %m");
#else
perror("nntpxfer: could not fdopen socket");
#endif
exit(1);
}
#ifdef SYSLOG
syslog(LOG_DEBUG,"connected to nntp server at %s", argv[1]);
#endif
#ifdef DEBUG
printf("connected to nntp server at %s\n", argv[1]);
#endif
/*
* ok, at this point we’re connected to the nntp daemon
* at the distant host.
*/
This is a fairly small example with only a few macros involved, yet reading the code is already painful. I've personally seen (and had to deal with) much worse in real code. Here the code is ugly and painful to read, but it's still fairly easy to figure out which code will be used under what circumstances. In many cases, you end up with much more complex structures.
To give a concrete example of how I'd prefer to see that written, I'd do something like this:
if (!open_history(HISTORY_FILE)) {
logerr(LOG_ERR, "couldn't open history file");
exit(1);
}
if ((server = get_nntp_connection(server)) == NULL) {
logerr(LOG_ERR, "couldn't open socket");
exit(1);
}
logerr(LOG_DEBUG, "connected to server %s", argv[1]);
In such a case, it's possible that our definition of logerr would be a macro instead of an actual function. It might be sufficiently trivial that it would make sense to have a header with something like:
#ifdef SYSLOG
#define logerr(level, msg, ...) /* ... */
#else
enum {LOG_DEBUG, LOG_ERR};
#define logerr(level, msg, ...) /* ... */
#endif
[for the moment, assuming a preprocessor that can/will handle variadic macros]
Given your supervisor's attitude, even that may not be acceptable. If so, that's fine. Instead a macro, implement that capability in a function instead. Isolate each implementation of the function(s) in its own source file and build the files appropriate to the target. If you have a lot of platform-specific code, you usually want to isolate it into a directory of its own, quite possibly with its own makefile1, and have a top-level makefile that just picks which other makefiles to invoke based on the specified target.
Upvotes: 19
Reputation: 300099
I have seen 3 broad usages of #ifdef
:
NDEBUG
anyone ?)Each has the potential to create a huge mess of unmaintanable code, and should be treated accordingly, but not all of them can be dealt with in the same fashion.
1. Platform specific code
Each platform comes with its own set of specific includes, structures and functions to deal with things like IO (mainly).
In this situation, the simplest way to deal with this mess is to present a unified front, and have platform specific implementations.
Ideally:
project/
include/namespace/
generic.h
src/
unix/
generic.cpp
windows/
generic.cpp
This way, the platform stuff is all kept together in one single file (per header) so easy to locate. The generic.h
file describes the interface, the generic.cpp
is selected by the build system. No #ifdef
.
If you want inline functions (for performance), then a specific genericImpl.i
providing the inline definitions and platform specific can be included at the end of the generic.h
file with a single #ifdef
.
2. Feature specific code
This gets a bit more complicated, but is usually experienced only by libraries.
For example, Boost.MPL
is much easier to implement with compilers having variadic templates.
Or, compilers supporting move constructors allow you to define more efficient versions of some operations.
There is no paradise here. If you find yourself in such a situation... you end up with a Boost-like file (aye).
3. Compilation Mode code
You can generally get away with a couple #ifdef
. The traditional example is assert
:
#ifdef NDEBUG
# define assert(X) (void)(0)
#else // NDEBUG
# define assert(X) do { if (!(X)) { assert_impl(__FILE__, __LINE__, #X); } while(0)
#endif // NDEBUG
Then, the use of the macro itself is not susceptible to the compilation mode, so at least the mess is contained within a single file.
Beware: there is a trap here, if the macro is not expanded to something that counts for a statement when "ifdefed away" then you risk to change the flow under some circumstances. Also, macro not evaluating their arguments may lead to strange behavior when there are function calls (with side effects) in the mix, but in this case this is desirable as the computation involved may be expensive.
Upvotes: 8
Reputation: 57729
I prefer splitting the platform dependent code & features into separate translation units and letting the build process decide which units to use.
I've lost a week of debugging time due to misspelled identifiers. The compiler does not do checking of defined constants across translation units. For example, one unit may use "WIN386" and another "WIN_386". Platform macros are a maintenance nightmare.
Also, when reading the code, you have to check the build instructions and header files to see which identifers are defined. There is also a difference between an identifier existing and having a value. Some code may test for the existance of an identifier while another tests the value of the same identifer. The latter test is undefined when the identifier is not specified.
Just believe they are evil and prefer not to use them.
Upvotes: 2
Reputation: 39419
You should avoid #ifdef
whenever possible. IIRC, it was Scott Meyers who wrote that with #ifdef
s you do not get platform-independent code. Instead you get code that depends on multiple platforms. Also #define
and #ifdef
are not part of the language itself. #define
s have no notion of scope, which can cause all sorts of problems. The best way is to keep the use of the preprocessor to a bare minimum, such as the include guards. Otherwise you are likely to end up with a tangled mess, which is very hard to understand, maintain, and debug.
Ideally, if you need to have platform-specific declarations, you should have separate platform-specific include directories, and handle them appropriately in your build environment.
If you have platform specific implementation of certain functions, you should also put them into separate .cpp files and again hash them out in the build configuration.
Another possibility is to use templates. You can represent your platforms with empty dummy structs, and use those as template parameters. Then you can use template specialization for platform-specific code. This way you would be relying on the compiler to generate platform-specific code from templates.
Of course, the only way for any of this to work, is to very cleanly factor out platform-specific code into separate functions or classes.
Upvotes: 11
Reputation: 154007
Others have indicated the preferred solution: put the dependent code in
a separate file, which is included. This the files corresponding to
different implementations can either be in separate directories (one of
which is specified by means of a -I
or a /I
directive in the
invocation), or by building up the name of the file dynamically (using
e.g macro concatenation), and using something like:
#include XX_dependentInclude(config.hh)
(In this case, XX_dependentInclude
might be defined as something like:
#define XX_string2( s ) # s
#define XX_stringize( s ) XX_string2(s)
#define XX_paste2( a, b ) a ## b
#define XX_paste( a, b ) XX_paste2( a, b )
#define XX_dependentInclude(name) XX_stringize(XX_paste(XX_SYST_ID,name))
and SYST_ID
is initialized using -D
or /D
in the compiler
invocation.)
In all of the above, replace XX_
with the prefix you usually use for macros.
Upvotes: 1
Reputation: 104698
personally, I prefer to abstract that noise well (where necessary). if it's all over the body of a class' interface - yuck!
so, let's say there is a type which is platform defined:
I will use a typedef at a high level for the inner bits and create an abstraction - that's often one line per #ifdef
/#else
/#endif
.
then for the implementation, i will also use a single #ifdef
for that abstraction in most cases (but that does mean that the platform specific definitions appear once per platform). I also separate them into separate platform specific files so I can rebuild a project by throwing all the sources into a project and building without a hiccup. In that case, #ifdef
is also handier than trying to figure out all the dependencies per project per platform, per build type.
So, just use it to focus on the platform specific abstraction you need, and use abstractions so the client code is the same -- just like reducing the scope of a variable ;)
Upvotes: 1
Reputation: 212484
Not sure what you mean by "#ifdef is strict no", but perhaps you are referring to a policy on a project you are working on.
You might consider not checking for things like Mac or WIN32 or i386, though. In general, you do not actually care if you are on a Mac. Instead, there is some feature of MacOS that you want, and what you care about is the presence (or absence) of that feature. For that reason, it is common to have a script in your build setup that checks for features and #defines things based on the features provided by the system, rather than making assumptions about the presence of features based on the platform. After all, you might assume certain features are absent on MacOS, but someone may have a version of MacOS on which they have ported that feature. The script that checks for such features is commonly called "configure", and it is often generated by autoconf.
Upvotes: 1
Reputation: 409384
Many programs use such a scheme to make platform specific code. A better way, and also a way to clean up the code, is to put all code specific to one platform in one file, naming the functions the same and having the same arguments. Then you just select which file to build depending on the platform.
It might still be some places left where you can not extract platform specific code into separate functions or files, and you still might need the #ifdef
parts, but hopefully it should be minimized.
Upvotes: 3