Expert C++
上QQ阅读APP看书,第一时间看更新

Understanding preprocessing

preprocessor is intended to process source files to make them ready for compilation. A preprocessor works with preprocessor directives, such as #define#include, and so on. Directives don't represent program statements, but they are commands for the preprocessor, telling it what to do with the text of the source file. The compiler cannot recognize those directives, so whenever you use preprocessor directives in your code, the preprocessor resolves them accordingly before the actual compilation of the code begins. 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.

As a rule of thumb, avoid using macro definitions. Macros are error-prone and C++ provides a set of constructs that make the use of macros obsolete. 

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;