(the post is automatically translated by AI)
Introduction
The preprocessor runs before the compilation phase. As a pre-compilation step, it can handle several types of tasks, including conditional compilation (e.g., #if, #ifndef), file inclusion, and macro definitions. In this article, we’ll look at each of these preprocessor features one by one.
The Language of the Preprocessor
To communicate with the preprocessor, we first need to understand its language. A standard preprocessor directive consists of three parts:
- The
#character - A series of standard-defined commands or user-defined preprocessing commands
- A newline
In other words, any code line starting with # is a directive for the preprocessor!
Preprocessor: “Anything that doesn’t start with
#— I don’t recognize it!”
How many types of preprocessor language are there? The reference documentation 1 clearly lists several categories:
- Conditional inclusion 2
- Replacing text macros 3
- Source file inclusion 4
- Diagnostic directive 5
- Implementation defined behavior control 6
- Filename and line information 7
Conditional Inclusion
Conditional preprocessing begins with #if, #ifdef, or #ifndef, and can include any number of #elif, #elifdef, or #elifndef directives, followed by an optional #else. The block is terminated with #endif.
Example: we first define ABCD and map it to 2. Then we check whether ABCD is defined and print accordingly.
#define ABCD 2
#include <iostream>
int main () {
#ifdef ABCD
std::cout << "1: yes\n" << std::endl;
#else
std::cout << "1: no\n" << std::endl;
#endif
}
Replacing Text Macros
// Object-like macros
#define identifier replacement-list
// Function-like macros
#define identifier(parameters, ...) replacement-list
Macros substitute text using #define. Once defined, the compiler replaces every occurrence of identifier in the code with the replacement-list. The example #define ABCD 2 in the previous section is a simple object-like macro.
There are two forms: Object-like macros and Function-like macros.
Source File Inclusion
#include <h-chr-sequence> new-line
#include "q-char-sequence" new-line
This is the familiar header file inclusion. #include <stdio.h>, for instance, replaces the directive with the contents of stdio.h.
Diagnostic Directive
#error diagnostic-message
#warning diagnostic-message
Implementation Defined Behavior Control
#pragma pragma-params
pragma directives control compiler-specific behaviors, such as suppressing warnings. The ISO C standard 8 does not mandate specific pragma implementations, but several are common: #pragma STDC, #pragma unpack, #pragma once, etc.
pragma once
Most modern compilers support this 9. When seen in a header file, it means the file will only be parsed once during compilation, even if included from multiple places. This achieves the same goal as include guards (#ifndef ... #define ... #endif).
However, there are some caveats: #pragma once cannot distinguish between files with the same name in different directories, nor can it guard against system-level header conflicts.
pragma pack
#pragma pack(arg)
#pragma pack()
#pragma pack(push)
#pragma pack(push, arg)
#pragma pack(pop)
This family of directives sets the memory alignment for user-defined contiguous data structures. Setting arg determines the alignment size in bytes.
Filename and Line Information
#line lineno
#line lineno "filename"
Used to change the current preprocessor’s filename (__FILE__) and line number (__LINE__).
#include <cassert>
#define FNAME "test.cc"
int main()
{
#line 777 FNAME
assert(2+2 == 5);
}
// Output:
// test: test.cc:777: int main(): Assertion `2+2 == 5' failed.
Conclusion
In this article, we examined several preprocessor directive types: file inclusion, replacing text macros, conditional inclusion, and more. These preprocessing techniques let us write more flexible and efficient code, and transform the source before the compilation phase begins.