Part 12: The Preprocessor
The Preprocessor was inherited from the C programming language. It parses our code before the compiler and generates the final stream that will be compiled. Using its features we can perform some modifications to our code right before compilation.
Preprocessor directives
Preprocessor directives begin with hash symbol (#). This character must be the first of the line, but not necessarily on the first column. Then follows the directive keyword. Blanc space between the hash symbol and the keyword is allowed. Here is a list of the preprocessor directives:
#include
This is the first preprocessor directive we learn as C/C++ developers. This instructs the preprocessor to include or inject the contents of another file at the current location. We mainly use it to include a header file with declarations.
#define
This directive is used to define a new name or macro. This macro can be used to modify the code the preprocessor outputs. Whenever the preprocessor encounters the macro, replaces it with its value:
#define PI 3.1415926
int main() {
double radius = 1.234;
double circumference = 2 * PI * radius;
}
It is a good way to replace difficult to remember values with easy to read and remember names. If we ever decide to increase the accuracy by adding digits to PI we change the definitions and recompile.
It can be used just to define a macro without giving it a value
#define __DEBUG__
#ifdef / #ifndef / #else / #endif
Check if a compiler directive has been defined and take appropriate action.
#include <iostream>
#define __DEBUG__
int main() {
#ifdef __DEBUG__
std::cout << "debug enabled\n";
#else
std::cout << "release enabled\n";
#endif
}
We can use this to safeguard our headers so they do not load twice:
#ifndef __PREPROC_H__
#define __PREPROC_H__
// this will not lead to recursion!!
// #ifndef in the first line prevents it
#include "preproc.h"
#endif // __PREPROC_H__
This is a very common situation in large projects where one header loads another and so on until they make a circle and compilation breaks because it sees the same definitions again.
#error
This directive raises a compilation error and stops the compilation process:
#ifndef __PREPROC_H__
#error include preproc.h to proceed
#endif
#pagma
Issue a special command to the compiler or the linker. The example dictates the compiler to ignore a specific warning and the linker to link our code with OpenGL:
#pragma warning( disable : 4705 )
#pragma comment( lib, "opengl32.lib" )
Predefined preprocessor macros
The preprocessor has some built-in macros we can access in our code. They can be particularly useful when displaying error messages which can be enhanced with details about their location within our code.
__LINE__: an integer containing the current line of code
__FILE__: a string containing the current file name
__DATE__: a string containing the compilation date in the form ‘MMM DD YYYY’
__TIME__ : a string containing the compilation time in the form ‘hh:mm:ss’
__cplusplus: it is a long integer holding the version of the C++ standard the compiler supports. Possible values are:
- std:c++14 201402L
- std:c++17 201703L
This macro is extremely useful when we want to interface C++ with C code:
#ifdef __cplusplus
// the following definitions follow the C standard
// note the opening bracket, look for the closing bracket
extern "C" {
#endif
// function that can be called both from C and C++ code
void some_function();
#ifdef __cplusplus
// end of C type definitions
// after the bracket we have C++ style definitions
}
// C++ definitions can follow (always inside #ifdef/#endif block!)
class some_class {
public:
some_class() {}
};
#endif
Summary
In this part we tried to give a brief description of the preprocessor. A valuable tool that if used correctly can help us solve many problems.