Conditional Compilation in C

The power of the C language preprocessor lies not only in the ability to define macros or include files. Through it, it is possible to perform so-called conditional compilation.

Through conditional compilation, it is possible to include or exclude parts of code from the program. The exclusion occurs at compile time, not at run time. This means that the excluded parts of code will not be present in the compiled program.

In this lesson, we will see which preprocessor directives allow conditional compilation and we will study their most common uses.

Conditional Compilation

Conditional compilation is a technique that allows including or excluding entire portions of code from the program based on certain conditions.

It is a technique applied at compile time. In fact, while in the case of conditional instructions, such as if or switch, the control over which instructions to execute is performed during the program execution itself, in the case of conditional compilation, the excluded instructions will be completely absent from the compiled program.

Therefore, the conditions that determine which parts of code to include or exclude must be known to the compiler, and cannot depend on values calculated at run time.

The C preprocessor provides directives for conditional compilation, which allow including or excluding parts of code based on the value of a macro.

Let's see in this lesson which preprocessor directives allow conditional compilation.

Definition

Conditional Compilation

Conditional compilation is a technique that allows including or excluding parts of code from the program based on the result of a test performed by the preprocessor.

#if and #endif Directives

A first use of conditional compilation concerns the possibility of compiling a program in two different modes: debug and release.

Typically, when a program is under development, there is a tendency to insert into the program itself additional instructions and functionalities that allow performing checks and debugging of the code. One of the typical additional functionalities is the possibility of printing debug messages to the screen, to verify the correct functioning of the program. In this case, we say that the program is compiled in debug mode.

Then, when the program is ready to be distributed, we want the program to be as lightweight and fast as possible, and not to contain additional parts of code that are not strictly necessary for the program's functioning. In this case, we say that the program is compiled in release mode.

To manage these two compilation modes, we typically proceed in this way.

First, we define a macro that indicates the compilation mode. For example, we can define a DEBUG macro that, if equal to 1, indicates that the program is in debug mode, while if equal to 0, indicates that the program is in release mode.

#define DEBUG 1

The name of the macro is not important, but it is important that the value of the macro is defined so that it can be used as a condition for conditional compilation.

Subsequently, whenever we want to insert diagnostic code into our program, we can use the #if directive to include the code only if the DEBUG macro is defined and equal to 1.

For example:

#if DEBUG
    printf("Debug: the value of x is %d\n", x);
#endif

During precompilation, the preprocessor verifies the condition that follows the #if directive. In this case, the preprocessor's #if directive works exactly like the C's if instruction, in the sense that it considers as true any value different from 0.

If the condition is true, the preprocessor includes in the program the instructions that follow the #if directive up to the #endif directive that closes the block. Conversely, if the condition is false, the preprocessor excludes from the program the instructions between #if and #endif.

In practice, if the condition of the #if directive is false, the preprocessor deletes the enclosed lines of code. Therefore, the compiler, when it receives the modified source code as input, will find no trace of the lines of code between #if and #endif. The code will, therefore, not be included in the final program.

In the example above, if the DEBUG macro is defined and equal to 1, the debug message will be printed to the screen. If, instead, the DEBUG macro is not defined or is equal to 0, the debug message will not be included in the final program.

In this way, we can leave the debug prints and deactivate them by simply changing the value of the DEBUG macro.

Recapping:

Definition

#if and #endif Directive

The #if directive allows including or excluding parts of code based on the value of a macro. If the condition is true, the code between #if and #endif will be included in the final program.

The syntax is as follows:

#if condition
    // code to include
#endif

Let's make two observations. The first is that the condition of the #if directive can be a constant expression. By this we mean that the expression can also be complex but must be calculable at compile time.

The consequence is that we cannot use variables or functions that are evaluated at run time. For example, we cannot write:

/* ERROR: the value of x is not known at compile time */
int x;

/* ... */

#if x > 0
    // code to include
#endif

However, we can combine the values of different macros with complex expressions. For example:

#define DEBUG 1
#define VERBOSE 0
#define VERSION 5

#if DEBUG && VERBOSE
    // code to include
#endif

#if !VERBOSE && DEBUG
    // code to include
#endif

#if VERSION < 5
    // code to include
#endif

In the first case, the code will be included only if both the DEBUG and VERBOSE macros are defined and equal to 1. In the second case, the code will be included only if the DEBUG macro is defined and equal to 1, while the VERBOSE macro is defined and equal to 0. In the third case, the code will be included only if the VERSION macro is defined and less than 5.

Definition

Conditions of #if Directives

The conditions of #if directives can be constant expressions, that is, expressions that can be evaluated at compile time. The expressions can be composed of macro values, operators, and numeric constants.

The constraint is that the expressions must be evaluable at compile time.

The second observation concerns the definition of the macros present in the conditions. In particular, it is not necessary for the macros to be defined. If a macro is not defined, the preprocessor considers it as if it were defined and equal to 0.

Let's take the following example:

#if DEBUG
    printf("Debug: the value of x is %d\n", x);
#endif

If the DEBUG macro is not defined, the preprocessor considers the DEBUG macro as if it were defined and equal to 0. Therefore, the code will be excluded from the final program.

Similarly, we can write:

#if !DEBUG
    /* Code to insert in release mode */
#endif

In this case, if the DEBUG macro is not defined, the code will be included in the final program.

Definition

Undefined Macros

If a macro is not defined, the preprocessor considers it as if it were defined and equal to 0. Therefore, we can use undefined macros in the conditions of #if directives.

defined Operator

In addition to the string conversion operator, #, and the concatenation operator, ##, there is a third operator specific to the preprocessor: the defined operator.

This operator, when applied to an identifier, returns 1 if the identifier corresponds to a defined macro, and 0 otherwise.

This operator is often used in conjunction with the #if directive to check whether a macro is defined or not.

Using this operator we can, for example, modify the following code:

#if DEBUG
    printf("Debug: the value of x is %d\n", x);
#endif

to:

#if defined(DEBUG)
    printf("Debug: the value of x is %d\n", x);
#endif

However, there is a fundamental difference. The defined operator returns 1 if the macro is defined, regardless of its value - it is enough that it is defined.

So, if we write:

#define DEBUG 0

or we write:

#define DEBUG 1

in both cases the condition #if defined(DEBUG) will be true.

Therefore, when using defined it is not necessary to pass a value to the macro. It is enough to simply write:

#define DEBUG
Definition

defined Operator

The defined operator returns 1 if the identifier passed as an argument is defined, and 0 otherwise.

The syntax is:

defined(identifier)

#ifdef and #ifndef Directives

The #ifdef and #ifndef directives are abbreviations of the #if defined and #if !defined directives. Their purpose is precisely to check whether a macro is defined or not.

We can, for example, rewrite the following code:

#if defined(DEBUG)
    printf("Debug: the value of x is %d\n", x);
#endif

in this way:

#ifdef DEBUG
    printf("Debug: the value of x is %d\n", x);
#endif

Similarly, we can rewrite the following code:

#if !defined(DEBUG)
    /* Code to insert in release mode */
#endif

in this way:

#ifndef DEBUG
    /* Code to insert in release mode */
#endif
Definition

#ifdef and #ifndef Directives

The #ifdef directive checks if a macro is defined, while the #ifndef directive checks if a macro is not defined.

The syntax is:

#ifdef identifier
#ifndef identifier

#elif and #else Directives

For convenience, the preprocessor also provides the #elif and #else directives that can be used in conjunction with the #if, #ifdef and #ifndef directives.

The syntax to use is as follows:

#if condition1
    // code to include if condition1 is true
#elif condition2
    // code to include if condition1 is false and condition2 is true
#else
    // code to include if none of the previous conditions is true
#endif

A possible example is:

#ifdef DEBUG
    printf("Debug: the value of x is %d\n", x);
#else
    /* Code to insert in release mode */
#endif

In a conditional code block, we can have multiple #elif directives and only one #else directive. The #else directive is optional.

Definition

#elif and #else Directives

The #elif and #else directives can be used in conjunction with the #if, #ifdef and #ifndef directives.

The syntax is:

#elif condition
#else

A conditional code block can contain multiple #elif directives and only one #else directive.

Nested Conditional Code Blocks

The preprocessor allows nesting conditional code blocks. This means that we can insert a conditional code block inside another conditional code block. Just as happens with the if and else instructions in C.

For example, we can write:

#if condition1
    // code to include if condition1 is true
    #if condition2
        // code to include if condition1 and condition2 are true
    #endif
#else
    // code to include if condition1 is false
#endif

In this case, the code between #if condition2 and #endif will be included only if the condition condition1 is true and the condition condition2 is true.

Hint

Suggestion for Writing Nested Conditional Code

When using nested conditional code, it is advisable not only to indent the code correctly, but also to use comments to indicate which code block each #if, #elif, #else and #endif directive belongs to.

For example:

1
2
3
4
5
6
7
8
#if condition1
    // code to include if condition1 is true
    #if condition2
        // code to include if condition1 and condition2 are true
    #endif /* condition2 */
#else
    // code to include if condition1 is false
#endif /* condition1 */

In this example we have indented the code and we have inserted comments at lines 5 and 8 to indicate which code block the #endif directives belong to.

Uses of Conditional Compilation

We have seen that one of the typical uses of conditional compilation is diagnostics or code debugging. In this case, we can insert screen prints or checks that allow us to verify the correct functioning of the program. Then, when the program is ready to be distributed, we can deactivate the debug prints by simply changing the value of a macro.

The uses of conditional compilation are not limited to this. Let's see, in this section, some of the most common uses.

Code Portability

Often the need arises to write a program for multiple operating systems, for example Linux, Windows and MacOS. Each operating system has specific particularities that require the use of different functions or libraries.

One might think of writing three different versions of the program, one for each system. However, this solution is not optimal, as it would require maintaining three versions of the code, with the risk of introducing errors or bugs.

An alternative is to use conditional compilation to include in the program the code parts specific to each operating system.

The compilers of these operating systems, in fact, define predefined macros that indicate the operating system for which the program is compiled. For example, the Linux compiler defines the __linux__ macro, the Windows compiler defines the _WIN32 macro, and so on.

We can, therefore, write the code in this way:

#ifdef __linux__
    // Code specific to Linux
#elif _WIN32
    // Code specific to Windows
#elif __APPLE__
    // Code specific to MacOS
#endif

When code is written in this way, we speak of portable code, that is, code that can be compiled and executed on multiple operating systems without having to make changes to the source code.

Software Versions

Another use of conditional compilation is managing different versions of a software. For example, one might want to distribute a lite version of a software that contains only the basic functionalities, and a pro version that contains additional functionalities.

In this case, one can define a macro that indicates the software version, and use conditional compilation to include in the program the code parts specific to each version.

For example:

#define VERSION_LITE 1
#define VERSION_PRO 2

#define VERSION VERSION_LITE

#if VERSION == VERSION_LITE
    // Code specific to the lite version
#elif VERSION == VERSION_PRO
    // Code specific to the pro version
#endif

In this way, it will be necessary to write only one version of the code, and use conditional compilation to include in the program the code parts specific to each version.

Compilation with Different Compilers

With conditional compilation we can also manage the differences between different compilers. Each compiler defines predefined macros that indicate the compiler itself. For example, the GCC compiler defines the __GNUC__ macro, the Clang compiler defines the __clang__ macro, and so on.

The differences between compilers may concern, for example, the implementation of some language functionalities, or the management of some language extensions.

Moreover, one compiler might support a different language standard than another compiler. For example, an old compiler might support only the C89 standard, while a more recent compiler might support the C99 or C11 standard.

In this case, we can write the code in this way:

#ifdef __GNUC__
    // Code specific to the GCC compiler
#elif __clang__
    // Code specific to the Clang compiler
#endif

#ifdef __STDC_VERSION__
    #if __STDC_VERSION__ >= 199901L
        // Code specific to compilers that support the C99 standard
    #endif
#endif

In this way, we can write the code taking into account the differences between different compilers.

Default Macro Definitions

Finally, another use of conditional compilation is to check whether a specific macro has been defined and, if not, define it with a default value.

For example, suppose we want to check if the BUFFER_SIZE macro has been defined, which indicates the size of the buffer to use in the program. If the macro has not been defined, we can define it with a default value:

#ifndef BUFFER_SIZE
    #define BUFFER_SIZE 1024
#endif

In this way, if the BUFFER_SIZE macro has not been defined, the preprocessor will define it with a default value equal to 1024.

In Summary

In this lesson we have seen how to use conditional compilation to include or exclude parts of code from the program based on the value of a macro.

We have studied new preprocessor directives:

  • #if and #endif to include or exclude parts of code based on the result of a constant expression;
  • #ifdef and #ifndef to check whether a macro is defined or not;
  • #elif and #else to manage multiple conditions;
  • the defined operator to check whether a macro is defined.

We have seen that conditional compilation is a very powerful technique that can be used for different purposes, such as code diagnostics, software portability, software version management and differences between compilers.

Furthermore, we have seen how to use conditional compilation to define macros with default values, if they have not been defined by the user.

In the next lesson we conclude the chapter on the C preprocessor, studying the remaining directives that the preprocessor makes available.