When a C++ source file is processed by the compiler, several phrases of translation take place. This time we will focus on the Phrase 4 - a stage when the preprocessor is executed.

…but what is the preprocessor?
Preprocessor is a component of the compiler that runs before the actual compilation. It can be used to include external files, define text substitutions (macros) or give some instructions to the processor. Generally, it works with directives that start with # symbol.

Almost every C++ program uses the preprocessor - when we add a library using #include <library> it’s a preprocessor directive. Similarly with #define, #undef, #ifdef, #ifndef, #if, #elif, #else, #endif, #pragma and more. All of them can be used in a basic, very popular way. However, the preprocessor have some more skills, which seem to be a little bit less known.

Stringizing operator

The first trick can be displaying the name and the value of the macrodefintion’s parameter. Use of the macrodefinition with stringizing operator allows to print monitored variables in a fast way. Let’s check it in the example:

#define SHOW(param) std::cout << #param << " = " << param << std::endl;

// some code

int x = 4;
// output: x++ = 4

// output: x = 5

// output: ++x = 6

SHOW(2. * static_cast<double>(x) / 7.);
// output: 2. * static_cast<double>(x) / 7. = 1.71429

Token-pasting operator

The second uncommon usage of the preprocessor is concatenating tokens - values or even fragments of names. It can be used also for meta programming to generate unique function names. Let’s see some examples how it works. We’ll use previously described macrodefinition SHOW(param):

#define CONCATENATE(x, y) x##y

// some code

int concatenatedValue = CONCATENATE(10, 9);
// output: concatenatedValue = 109

int badStudentMark = 2, goodStudentMark = 5;
SHOW(CONCATENATE(bad, StudentMark));
// output: CONCATENATE(bad, StudentMark) = 2
SHOW(CONCATENATE(good, StudentMark));
// output: CONCATENATE(good, StudentMark) = 5

Predefined names

The preprocessor not only allows to create custom rules but also provides some predefined names. They can be used to generate errors or warnings (e.g. __cplusplus can be used to check if the C++ standard in acceptable) or simply get some information (e.g. display error location in file). Let’s see it in action:

std::cout << "C++ standard: " << __cplusplus << '\n';
// example output: C++ standard: 202002
std::cout << __FILE__ << ":" << __LINE__ << " (func: " << __func__ << ")\n";
// example output: main.cpp:46 (func: main)
std::cout << "Compilation: " << __DATE__ << ", " << __TIME__ << std::endl;
// example output: Compilation: Jun 26 2023, 14:54:31

Conditional compilation

More common (but still worth to be mentioned here) usage of the preprocessor is conditional compilation. Thanks to it the preprocessor can use an appropriate code to compile. Let’s see some basic example (which also uses previously defined SHOW(param) macro):

#ifdef DEBUG
	#define CONFIG_PATH "config"
#elif _WIN32
	#define CONFIG_PATH "%APPDATA%\\MyApp"
#elif __APPLE__
	#define CONFIG_PATH "~/Library/Application Support/MyApp"
#elif __linux__
	#define CONFIG_PATH "/usr/local/share/MyApp/config/"
	#define CONFIG_PATH "config"
#endif //CONFIG_PATH

// some code

// example output: CONFIG_PATH = /usr/local/share/MyApp/config/

Here you can find the full code from the examples.

Preprocessor is a powerfull tool that can be very helpful in some situations. It provides some kind of an interaction between your program and a compilator. I highly encourage you to experiment a bit with it and discover some ways you can use it.

If you liked this post, be sure to check some other ones and revisit this blog in the future.

See you soon! 😸