
Understanding preprocessing
For example, the following code will be changed before the compiler starts to compile it:
#define NUMBER 41
int main() {
int a = NUMBER + 1;
return 0;
}
Everything that is defined using the #define directive is called a macro. After preprocessing, the compiler gets the transformed source in this form:
int main() {
int a = 41 + 1;
return 0;
}
As already mentioned, the preprocessor is just processing the text and does not care about language rules or its syntax. Using preprocessor directives, especially macro definitions, as in the previous example, #define NUMBER 41 is error-prone, unless you realize that the preprocessor simply replaces any occurrence of NUMBER with 41 without interpreting 41 as an integer. For the preprocessor, the following lines are both valid:
int b = NUMBER + 1;
struct T {}; // user-defined type
T t = NUMBER; // preprocessed successfully, but compile error
This produces the following code:
int b = 41 + 1
struct T {};
T t = 41; // error line
When the compiler starts compilation, it finds the assignment t = 41 erroneous because there is no viable conversion from 'int' to 'T'.
It is even dangerous to use macros that are correct syntactically but have logical errors:
#define DOUBLE_IT(arg) (arg * arg)
The preprocessor will replace any occurrence of DOUBLE_IT(arg) with (arg * arg), therefore the following code will output 16:
int st = DOUBLE_IT(4);
std::cout << st;
The compiler will receive this code as follows:
int st = (4 * 4);
std::cout << st;
Problems arise when we use complex expressions as a macro argument:
int bad_result = DOUBLE_IT(4 + 1);
std::cout << bad_result;
Intuitively, this code will produce 25, but the truth is that the preprocessor doesn't do anything but text processing, and in this case, it replaces the macro like this:
int bad_result = (4 + 1 * 4 + 1);
std::cout << bad_result;
This outputs 9, and 9 is obviously not 25.
To fix the macro definition, surround the macro argument with additional parentheses:
#define DOUBLE_IT(arg) ((arg) * (arg))
Now the expression will take this form:
int bad_result = ((4 + 1) * (4 + 1));
It is strongly suggested to use const declarations instead of macro definitions wherever applicable.
The same preceding example would be type-checked and processed at compile time if we used a constexpr function:
constexpr int double_it(int arg) { return arg * arg; }
int bad_result = double_it(4 + 1);
Use the constexpr specifier to make it possible to evaluate the return value of the function (or the value of a variable) at compile time. The example with the NUMBER definition would be better rewritten using a const variable:
const int NUMBER = 41;