A means of controlling the generation of a computer
program based upon conditions specified at the time it is
compiled (translated
from its
source code into an intermediate form, or into a
machine language
that the computer can directly process).
Conditional compilation is used mainly for:
-
Isolating blocks of code dependent upon a particular platform and/or compiler,
and providing alternate blocks of code when the dependency is not met,
-
Preventing
the same code from being compiled more than once due to the code's source
file being accessed more than once by the compiler (the famous #include guard), and
-
Turning various facilities in the source code (such as debug statements)
on or off as the programmer wishes.
The most widely recognized form of conditional compilation arises from
the introduction of particular
compiler directives into a program's source
code.
This, of course, means that the compiler must provide facilities
to support it.
The best-known (and most widely copied) of these
facilities are the family of
#if compiler directives that
are used in the
C and
C++ programming languages.
Conditional compilation in C/C++
Because the traditional C compiler,
cc, implements conditional
compilation facilities in its
preprocessing phase (and because all C
and C++ compilers must behave as if the preprocessing phase takes place
separately), these facilities take the form of
preprocessor directives:
lines of code that begin with a
# character and which are followed
by a word naming the directive.
Blocks of C and C++ code that might be compiled, and also might not,
are placed in between a #if directive and a #endif
directive:
#if compile-time-expression
//
// some code
//
#endif
If, at the time the program is compiled, the compile-time-expression
evaluates
to a true or nonzero value, the compiler evaluates all of the code between
the #if and the #endif. If not, the
lines between the #if and the #endif are
skipped.
Variations on the basic theme.
The directive
#else is used when code also exists for those
occations when the
compile-time-expression evaluates to
false.
so instead of
#if compile-time-expression
//
// some code
//
#endif
#if negative-form-of-above-compile-time-expression
//
// some more code
//
#endif
we have
#if compile-time-expression
//
// some code
//
#else
//
// some more code
//
#endif
which is easier to understand, and less error-prone.
The directive #elif combines a #else directive
and the #if directive of a new condition.
It is not very useful, for reasons we will describe below.
#if blocks may be
nested: That is, the lines between
a
#if ...
#endif pair may contain a block
of lines between another
#if ...
#endif
pair. Thus:
#if foo
//
// some code
//
# if bar
//
// some more code.
// This will be compile only if both
foo and bar are true.
//
# endif // to balance #if bar
//
// yet more code in the #if foo block
//
#endif // to balance #if foo
The basic
#if directive has two very important variations:
#ifdef macro evaluates to a true value only if
the specified preprocessor macro is defined when the compiler
reaches the directive.
Similarly, #ifndef macro evaluates to a true
value only if the specified macro is not defined.
Due to the nature of the primary uses of conditional compilation described
above, #ifdef and #ifndef are traditionally
used much more frequently than #if (the reason
#elif isn't very useful).
This has recently changed in the C++ world. Due to the complexity
of the compile-time expressions that programmers found they had to use
in C++, the construction defined(macro) was
added to the preprocessor language. This allows checking the definition
/ non-definition of more than one macro in the same expression.
Isolating platform dependencies
Platform dependencies are isolated by using
#ifdef with
a macro named for the platform. These are either pre-defined in the
compiler, or set in the
makefile used to control compiling the entire
project. Thus:
#ifdef _MSCVER
//
// This is Microsoft Visual C or Visual C++
//
#if _MSCVER > 600
//
// Visual C++ 6.0 or better
//
# endif
#endif
#ifdef _BORLANDC_
//
// it's Turbo C or Borland C++ Builder
//
#endif
#ifdef __MWERKS__
//
// It's CodeWarrior
//
#endif
#ifdef POSIX
//
// POSIX-compliant operating system
//
#endif
#ifdef __cplusplus
//
// it's C++ and not C
//
#endif
There are all sorts of constructs you can make, for the compiler version,
processor version, operating system, and things like that.
Include guards
Include guards arise from the traditional use of include files to specify
a C module's interface to the outside world.
Let's say you have a module
foo whose interface you
have packaged neatly into the file
foo.h. Let's
say you also have two other modules,
foo_user1 and
foo_user2
which both depend on the module foo. You've packaged their interfaces
into foo_user1.h and foo_user1.h:
// -- foo_user1.h --
#include "foo.h"
...
... foo_user1 interface
// -- foo_usr2.h --
#include "foo.h"
...
... foo_user2 interface
Finally, let's say you have a module bar which uses
both foo_user1 and foo_user2, but doesn't
need to know about foo's interface. It has
to include foo_user1.h and foo_user2.h:
// -- bar.c --
#include "foo_user1.h"
#include "foo_user2.h"
Since foo.h will be included more than once, the above
construction will be rejected by the compiler somewhere in the bowels of
foo.h the second time around, the moment something is defined
the second time. A solution might be to remove the references
to foo.h from foo_user1.h and foo_user2.h
The solution is to make up a macro name for each file that might be
#include'ed, and then to wrap the contents of the file
in a #if construction like this:
#ifndef THIS_HERE_INCLUDE_FILE_H
#define THIS_HERE_INCLUDE_FILE_H
//
// All the rest of the code in the file
//
#endif
The trick is to #define the macro so that the
#ifndef directive can be true only once in a particular translation
unit.
In this way, the contents of the include file will be processed the
first time it is accessed because of a #include directive
in another file. If a later #include directive mentions
the same file, however, the contents won't be processed again.
It has become fashionable in some circles to place the #include guard
around the #include directive rather than inside the included
file. Don't do this. Although this technique reduces dependencies
in automatic makefile generators, it obfuscates your code.
Another phenomenon you will run into frequently is the construction
#pragma once, which is a facility provided by some compiler manufacturers
(most notably Microsoft) to do precisely the same thing. But it's
a platform dependency, which you have to isolate with... conditional compilation.
Plus, you have to provide alternative code for platforms that don't support
it, which means... an include guard.
Switching facilities on and off
During the programming cycle, you may have differing purposes for invoking
a compiler: At one point, you may wish to create a program you can
debug; at other times, you may be finished debugging (ha!) and may wish
to create a sleek, optimized program. To that end, you might create
the following construction:
#ifdef DEBUG
#pragma take_it_easy_on_me_O_mighty_compiler
#else
#pragma make_me_a_lean_mean_virtual_machine
#endif
Although symbolic debuggers are nice, sometimes nothing beats a good
old print statement for diagnosing your problem.
You might then have:
#ifdef DEBUG
cout << "i = " << i << "; I hope you're satisfied."
<< endl
#endif
This, of course, has evolved into the famous ASSERT macro:
#ifdef _NDEBUG
#define ASSERT(cond) if (!cond) { cout << "Hey idiot,
(" << ## cond << ") doesn't hold!" << endl; crash ();
}
#else
#define ASSERT(cond)
#endif
If _NDEBUG is on, the program contains code to test
the condition at the point of the assertion of the macro.
If the condition is false, the program crashes and an error message
is printed.
In production code, you have assured youself that the condition will
always (ha again!) hold. Therefore, you turn _NDEBUG off, and the
extra code doesn't appear in the final program.