Basics of using the preprocessor

Anyone who has ever read a piece of C source code has seen them—the preprocessor directives. For example, you can find include directives (#include) at the beginning of most source files. The preprocessor is a system that rewrites the source before the actual compiler sees it. Clearly, this is a very powerful tool—the downside is that you could accidentally shoot yourself in the foot.

In this article we will introduce the preprocessor and covered the basics, including object- and function-like macros, the include directive, conditional compilation, and end with the two special directives #error and #pragma.

The #include directive

The most straight-forward preprocessor directive is #include. When the preprocessor finds this directive it simply opens the file specified and inserts the content of it, as though the content of the file would have been written at the location of the directive.

It can take two forms, for example:

#include <systemfile.h>
#include "myfile.h"

The first is used to include standard headers like stdio.h. The latter is for your own application-specific headers.

Macros

One of most useful features of the preprocessor is to allow the user to define macros, which simply is an identifier that is mapped to a piece of source code. Whenever the preprocessor finds the macro in the application source code it replaces the macro with the definition.

Basically, there are two types of macros, object-like macros and function-like macros, the difference is that function-like macros have parameters.

By convention, macro names are written using upper-case only. The only exception is when a macro is used to replace something that should have been a function but is implemented using a macro for the sake of efficiency.

The directive #define can be used to define a macro. In the following example, we define NUMBER_OF_PLAYERS to the constant "2". This is an object-like macro.

#define NUMBER_OF_PLAYERS 2

int current_score[NUMBER_OF_PLAYERS];

Here, the function-like macro PRINT_PLAYER is mapped a more complex piece of source code.

#define PRINT_PLAYER(no) printf("Player %d is named %s", no, names[no])

 

The definition of a macro should, technically, be specified on a single source line. Fortunately, the C standard allows you to end a line with a backslash and continue on the next physical line. For example:

#define A_MACRO_THAT_DOES_SOMETHING_TEN_TIMES \
for (i = 0; i < 10; ++i)\
{ \

do_something(); \
}

In addition to the macros that a user can define, the C standard specifies a number of predefined and library-provided macros that could be used. For example, the macro __FILE__ contains the name of the current source file, as a string.

If you ever would like to make a macro name undefined you can use the #undef directive.

Object-like macros

Object-like macros can be used to replace an identifier in the source code with some kind of replacement source code.

Typically, macros can be used to declare a constant that could be configured in one location. Also, constants could be used to make the source code more readable, even if the value is not intended to change. For example:

#define SQUARE_ROOT_OF_TWO 1.4142135623730950488016887
double convert_side_to_diagonal(double x)
{
return x * SQUARE_ROOT_OF_TWO;
}
double convert_diagonal_to_side(double x)
{
return x / SQUARE_ROOT_OF_TWO;
}

Preprocessor macros could be used for very weird things, since all that the preprocessor does is to replace an identifier with an arbitrary piece of source code. For example, the following is legal code (although, you will probably have to answer to your boss if you ever try to write something like this):

#define BLA_BLA );
int test(int x)
{
printf("%d", x BLA_BLA
}

Function-like macros

Function-like macros are macros that take parameters. When you use them they look like a function call. A function-like macro could look like the following:

#define SEND(x) output_array[output_index++] = x

When the preprocessor finds a function-like macro in your application source code it will replace it with the definition. The parameters of the macro will be inserted into the resulting source code at the location of the formal parameter variables.

So, if you write the following:

SEND(10)

Then the compiler will see:

output_array[output_index++] = 10

In the second part of this article we will revisit function-like macros and discuss some of the traps that are easy to fall into.

Conditional compilation

One of the most powerful features of the preprocessor is the so-called conditional compilation—this means that portions of the code could be excluded in the actual compilation under the certain conditions.

This means that your source could contain special code for, say, the ARM processor. Using conditional compilation, this code could be ignored when compiling for all other processors.

The preprocessor directives #ifdef#ifndef#if#elif, and #else are used to control the source code. The#ifdef (#ifndef) directive includes a section if a preprocessor symbol is defined (undefined). For example:

#ifdef ARM_BUILD
__ARM_do_something();
#else
generic_do_something();
#endif

The #if directive can handle any type of integer and logical expression, for example:

#if (NUMBER_OF_PROCESSES > 1) && (LOCKING == TRUE)
lock_process();
#endif

The #elif directive works like a combined #else and #if.

#if and #elif can use the special operator defined to check if a symbol is defined. This is useful in combination with complex tests, for example:

#if defined(VERSION) && (VERSION > 2)
...
#endif

In part two of this article we will revisit conditional compilation and discuss whether you should prefer #if:s or #ifdef:s in your application.

Include guards

One typical use for conditional compilation is to ensure that the content of include files are only seen once. This will not only speed up the compilation, but also ensures that the compiler will not issue an error (e.g. for redeclaration of a struct) if the header file is included twice.

An include guard typically looks like the following:

#ifndef MYHEADER_H
#define MYHEADER_H

/* The content of the header file goes here. */

#endif

Clearly, the first time the header file is included the symbol MYHEADER_H is not defined and the content is included in the compilation. The second time the header file is read the symbol is defined and the content is excluded.

Error directives

The #error directive can be used to generate a compiler error message. This is useful when performing consistency checks, for example:

#if USE_COLORS && !HAVE_DISPLAY
#error "You cannot use colors unless you have a display"
#endif

The #pragma directive

Another preprocessor directive is #pragma. This directive allows the programmer to control the behavior of the compiler and gives compiler vendors the opportunity to implement extensions to the C language.

The #pragma directice is not covered further in this article since it has little to do with the main task of the preprocessor.