Preprocessor & Macros
The preprocessor is a textual pass that runs before the compiler, rewriting the source via #include, #define, and #if directives into a single translation unit.
Why it matters
It is the mechanism behind every header inclusion, conditional compilation (#ifdef _WIN32), and the assert/feature-detection machinery that real codebases lean on. But because it operates on tokens with no knowledge of types, scope, or namespaces, macros are the source of some of the nastiest bugs in C++ — and modern features (constexpr, templates, modules-c-20) exist largely to replace them.
How it works
The preprocessor runs as an early phase of the compilation model and emits text the compiler then parses. Key directives:
| Directive | Purpose |
|---|---|
#include | paste a file’s contents verbatim |
#define X v | object-like macro: replace X with v |
#define F(a) | function-like macro: token substitution |
#if / #ifdef / #endif | conditional compilation |
#pragma once | include guard (non-standard but universal) |
#error | abort compilation with a message |
- Function-like macros substitute tokens, not values: arguments are re-evaluated each time they appear, so
MAX(a++, b)can incrementatwice. #stringizes an argument;##pastes tokens together. Double-expansion macros (STR(X)callingSTR_(X)) are needed to stringize a macro’s value rather than its name.__FILE__,__LINE__,__func__, and__cplusplus(e.g.202002Lfor C++20) are predefined; test feature support with__has_include/__cpp_*macros.- Guard every header against double inclusion with
#pragma onceor an#ifndef PROJ_FOO_Htriad — see header-source-separation.
Example
#define SQUARE(x) ((x) * (x)) // parens are mandatory
int a = SQUARE(1 + 2); // ((1+2)*(1+2)) = 9, not 1+2*1+2 = 5
int b = SQUARE(a++); // a incremented TWICE -> UB-ish surprise
// prefer the typed, single-evaluation alternative:
constexpr auto square(auto x) { return x * x; }The macro version has no type checking, ignores namespaces, and breaks debuggers; the constexpr one is type-safe and evaluates x once.
Pitfalls
- Missing parentheses around parameters and the whole body cause precedence bugs (
SQUARE(1+2)), and multiple evaluation of side-effecting arguments silently doubles work. - Macros ignore scope and case —
#define max(...)collides withstd::max; including<windows.h>definesmin/maxmacros that wreck the STL unless you#define NOMINMAX. - No debugger visibility: macro-expanded code has no symbols, so you cannot step into or breakpoint inside a macro.
#ifdeftypos compile silently: a misspelled or never-defined name just evaluates false, so a whole block vanishes with no error.