Preprocessor Directives

So what are the preprocessor directives?

They are the instructions that begin with the hash sign #. When you Build the program, the preprocessor scans through the source files and executes the preprocessor directives. then the code is compiled. The core compiler doesn't see the commands that begin with #, they are resolved beforehand.

There are a handful of preprocessor directives. We will review Macro definitions, conditional inclusions and header file inclusions.

Preprocessor Directives are lines in the code of the program preceded by a hash sign #

They are not part of the compiled program. They are executed during building of the program, in the preprocessor step. They include:

Macros

Macros are a handy way of creating an alias for a C/C++ piece of instruction.

#include <cstdint>
#include <iostream>

#define PI 3.1416f    /* Can be used to define constants */
#define GPS_ENABLED 1 /* Can be used as a global to enable/disable subroutines */
#define FIRMWARE_V 4  /* Can be used to denote version of hardware  software */
#define DEBUG 0
#define RAD2D(x) x * 180.0 / PI /* A Macro can be function-like, taking & pro-cessing an argument */

void update_gps() {}
void update_gps_2() {}

int main(int argc, char *argv[])
{
    float rad = 0.1;
    float circumference = 2 * PI * rad; /* Useful instead of writing 3.1416 every time */
    if (GPS_ENABLED)
    {
        update_gps();
    }
    if (FIRMWARE_V > 2)
    {
        update_gps_2();
    }
    if (DEBUG)
    {
        std::cout << "Log Message" << std::endl;
    }
    return 0;
}

To use the macro definitions. We include the #define keyword, the macro definition and then what it stands for.

You will often find macro definitions used in place of some flags, numbers, version number. And then can also be function like where they take and process an argument.

Note that the preprocessor executes these commands. The compiler will not see the macro definitions.

For example, PI is macro defined as 3.1416, The preprocessor will look for all instances of the word PI in the code and replace it with 3.1416 BEFORE the code is compiled.

Here we define a function macro to convert rad to degrees. When used in code, the preprocessor will replace RAD2D(PI/2) for example to 1.5 * 180 / pi. This is not a function, but a command that will be substituted in the code whenever the macro RAD2D(x)

#define RAD2D(x) x * 180.0 / PI

Conditional Inclusions

The preprocessor can perform a conditional if statement.

#include <iostream>
using namespace std;

#define FW_VER 2 /* We define some macro here, such as Firmware version */

/* Then we can define different functions / routines / objects etc. based on FW version*/
#if(FW_VER == 1)
    #define print_version() cout<<"Version 1" <<endl; 
#elif(FW_VER == 2)
    #define print_version() cout<<"Version 2" <<endl;
#endif

int main(int argc, char *argv[]){
    print_version(); /* Output: Version 2 */
    return 0;
}
Version 2

The syntax would require a #if and a #endif at least, with as many #elif in between.

One example use is when you have a code that you want to be backward compatible with previous versions of hardware or firmware.

You can set a macro somewhere to define the version number of your hardware for example, and then use the conditional inclusions to determine which piece of code to execute, depending on the hardware version.

Note that only the true statement code will be compiled. Think of it as a way to filter and select between different sets of codes to compile based on some condition. There is no overhead during the code execution, since the condition is fixed at compile time and doesn't change during execution (your drone hardware will not change midflight for instance, or maybe it will for some reason, that would be very creative).

Ifdef & Ifndef

In this example we define a macro FW_VER and set it to 2. Then we have conditional inclusion.

#include <iostream>
using namespace std;

#ifndef FW_VER /* We can check if a macro is undefined */
    #define FW_VER 1 
#endif

/* Then we can define different functions / routines / objects etc. based on FW version*/
#if(FW_VER == 1)
    #define print_version() cout<<"Version 1" <<endl; 
#elif(FW_VER == 2)
    #define print_version() cout<<"Version 2" <<endl;
#endif

int main(int argc, char *argv[]){
    print_version(); /* Output: Version 1 */
    return 0;
}
Version 1

Note that the second condition is true and the print_version() macro is defined as the following. Again, the compile will not see these hash lines, in this example it will see cout<<"Version 1" <<endl; here instead of print_version()

Another useful macro is the ifdef and ifndef directives. They can be used to check if a specific macro is defined or not defined.

#ifndef FW_VER /* We can check if a macro is undefined */
    #define FW_VER 1 
#endif

Assume you have a conditional inclusion statement (if elif) that relies on a certain macro being defined. Perhaps if the header file where it should be defined in is not included. You might want to add a default value to it then, as shown in the example here. Which checks to see if FW_VER is defined, if it is not, then it defines it to some value. Perhaps this is a default value.

Note that a macro doesn’t have to have an equivalent code. We can simply macro define fw_ver without any associated code or value and the ifdef directive would still find it as valid.

#define FW_VER /* This is sufficient to have FW_VER defined */
#ifndef FW_VER 
    #define FW_VER 1 
#endif

Header Inclusion

You have already seen this directive.

/* Quotes include looks for the header files in the working directory */
#include "my_header.h" 
/* Angle brackets are generally used for system headers */
#include <system_header.h> 

int main(int argc, char *argv[]){
    some_function_from_my_header();
    some_other_function_from_system_header();
    return 0;
}

The include directive as the name implies, includes the content of the included file. You can picture the preprocessor taking all the content of the included header and dumping it in place of the include line.

When using the double quotes to include a header, the preprocessor looks for the header file in the same folder as or with respective to the folder location of the source file.

/* Quotes include looks for the header files in the working directory */
#include "my_header.h"

When using angled brackets to include a header file, the preprocessor looks for the header in the system lib include paths. The include paths would be specified to the compiler configuration file (e.g. cmake, make, etc.)

/* Angle brackets are generally used for system headers */
#include <system_header.h>

Next: Standard Library