It’s very common to want to conditionally compile code; a typical example is to include logging for debugging purposes. This article examines the approaches taken in C/C++ and Java. I suggest an approach for languages that typically achieve conditional compilation using a preprocessor (including C and C++) that makes code easy to read and therefore cheap to maintain.
In C and C++, I have usually seen conditional compilation achieved via the preprocessor mechanism. For example, in C/C++:
#define DEBUG
/* ... some code ... */
#ifdef DEBUG
debug_log("Got here!"); /* debug_log() is a function that does the logging.*/
#endif
By #undefing DEBUG, the logging is
disabled.
As an aside, one can define a LOG macro that can be
conditionally compiled, for example:
#define DEBUG
#ifdef DEBUG
#define LOG(x) (debug_log(x))
#else
#define LOG(x)
#endif
/* ... some code ... */
LOG("Got here!"); // Expands to (debug_log("Got here!"))
Again, by #undefing DEBUG, the
LOG macro will do nothing, and the logging is
disabled.
These techniques, however useful, have major drawbacks:
CAPS, so they
disrupt the natural flow of C/C++ (which are typically written in
lowercase, or a variant of CamelCase);
Code that is inserted or removed by the preprocessor forces one to
think in a modal fashion:
“OK, so when DEBUG is defined, this code is inserted and then this happens, but when it’s not defined, the code will look like this and something else happens”
— there are two modes of thought required. If there is
another symbol that controls conditional-compilation, then there
are four modes of thought required (then 8, then 16 and so on).
Software is so complex that introducing such parallel universes is
asking for trouble. Additionally, debuggers do not generally
understand macro expansions, so the code one reads is not the
actual code the compiler sees — debugging is confounded by
the preprocessor. Errors that are hidden by the preprocessor are
especially hard to find. Since maintenance is usually the most
expensive part of the software development process, it’s
worth ensuring that it’s easy (and therefore cheap). One way
to do this is to make the code as readable as possible.
In Java, there is no (standard) preprocessor, so one cannot
conditionally include or exclude code. Instead, a constant global
variable is tested. The advantage of this approach is that the
program is non-modal: when reading the code you don’t have to
mentally run two different programs (in the case of a single
DEBUG symbol), or 4 or 8 or 16… You just treat
the debug variable like any other variable in your
code. The key point is that with macros, you have to think of your
code as two different programs at the same time; with a global
constant, you have one program that has more conditional paths of
execution. (As we’ll see shortly, these additional paths of
execution don’t have to translate into machine code.) Your
code also looks neater (IMHO), as you don’t have ugly MACRO
symbols all OVER the PLACE. One can use a global variable to
achieve conditional compilation in C++ as follows:
const bool debug = true; // Global scope
/* ... some code ... */
if(debug) debug_log("Got here");
In C, there is (to my knowledge) currently no boolean type defined by the standard. Instead of using
const int debug = (1==1);
it is better to define your own boolean type. You can do this, and use it, as follows:
typedef enum {false, true} Boolean;
const Boolean debug = true;
if(debug) debug_log("Got here!");
This works because enums are implemented as ints, and the
enumeration begins at zero, unless otherwise stated. So, if
debug is set to false, it is actually set
to zero, which is what the C standard defines to represent the
concept of falseness; true equates to 1 in the above
type definition, and C uses non-zero values to represent the
concept of truth. However, be aware that since the C standard does
not define a boolean type, holy wars are common on this topic. Some
argue that one must stick to the C standard’s definition of
false and true (i.e. zero and non-zero) but this is conceptually
troublesome — true and false should not map to numerical
values. The above type definition represents true and false
conceptually, and also works with the C standard.
The worry that most C and C++ programmers will have about this
approach to conditional compilation is efficiency. When we release
the code, we don’t want the logging function to be called,
and by compiling the code with debug set to false, it
won’t. However, we also don’t want to waste time
testing debug. In release code, we know that
debug is false, and so there is no need to even test
it. Your compiler should be able to realise this too, if you tell
the compiler to optimise your code (with GCC, use options -O, -O2
or -O3). (A look over the resulting assembly listing confirms
this.) By using program logic to achieve conditional compilation
instead of the preprocessor, readability is increased, the program
is easier to understand and maintenance is made cheaper.
Although I would encourage against using the preprocessor as much
as possible, I would recommend the use of the C and C++ standard
predefined macros such as __FILE__ and
__LINE__, which expand to the name of the current
input file (in the form of a C string constant) and the current
input line number (in the form of a decimal integer constant)
respectively. Readability is reduced by the ugly symbols, but by
using a logging function that accepts the source of an error in the
form of a filename and line number, debugging can be made much
easier. The key point here is that the developer is not forced to
think in a modal fashion — there is still only one program,
compared to the multiple programs created by the use of
preprocessor-driven conditional compilation. Using these predefined
macros, our conditionally-compiled code might look like:
const bool debug = true; // Global scope
/* ... some code ... */
if(debug) debug_log(__FILE__, __LINE__, "Some meaningful comment.");
The approach recommended in this article is also part of the GNU coding standards. They cite the ability of the compiler to perform “extensive checking of all possible code paths” rather than readability and maintainability, but I believe the real benefits come from being able to consider code in a non-modal way.
Most good modern software can detect when it has crashed and offers the user the chance to submit the information to the developers via the Internet. However, there may be software errors that do not cause such catastrophic failures and which are only reproducible on a client‘s site (which may be geographically distant). In this case, you may want to design to be able to turn on the type of logging that we have been discussing via a hidden option. This information can then be submitted via the Internet and inspected in the lab, providing a way to remotely find problems. If you do include such ’phone home’ technology, ensure you observe applicable data protection laws!