!!!Maintenance in Progress!!!

Conditional Compilation

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:

  1. the preprocessor forces one to think in a modal fashion;
  2. macros are typically given names in CAPS, so they disrupt the natural flow of C/C++ (which are typically written in lowercase, or a variant of CamelCase);
  3. if one uses macros, the code you read is not the same code that the compiler reads;
  4. macro usage tempts one to move ever more functionality onto the preprocessor, making the code hard to debug and maintain.

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!