Every C++ source file goes through the preprocessor before the compiler sees it. The preprocessor is a line-oriented text rewriter that recognizes lines beginning with # as directives and transforms the source according to them. This chapter covers the full directive set: #include for pulling in headers, #define and #undef for managing macro names, the conditional compilation family (#if, #ifdef, #ifndef, #elif, #else, #endif), #pragma for compiler-specific instructions, include guards, the diagnostic directives #error and #warning, and the rarely seen #line. The chapter closes with why modern C++ replaces most preprocessor uses with constexpr, inline, and enum class.
The preprocessor is a separate program that runs before the compiler proper. Given a file like shop.cpp, the preprocessor reads it top to bottom, performs textual transformations driven by directives, and produces a single flat translation unit that the compiler then parses. The compiler itself never sees #include or #define. By the time the parser starts, those directives have already been applied and erased.
The transformations the preprocessor performs are mechanical and local:
#include line with the full contents of the named file.#if/#ifdef conditions.#pragma directives to the compiler for special behavior.A directive is any line where the first non-whitespace character is #, followed by the directive name. The # must be the first non-blank token, but whitespace before it is allowed:
Directives end at the end of the line. To continue one across multiple lines, end the line with a backslash:
The preprocessor stitches the two lines together before applying the directive. The backslash must be the very last character before the newline. Stray whitespace after it breaks the continuation on some compilers, often without an error message.
To see what the preprocessor hands to the compiler, pass -E to g++ or clang++:
The resulting shop.i file contains the expanded text after preprocessing. It's often hundreds of thousands of lines long because of all the headers that iostream and friends pull in transitively. Reading the expanded output is one way to debug a confusing macro or a header-only-library that misbehaves.
#include tells the preprocessor to take the named file and paste its full contents at the point of the directive. There are two syntactic forms, and they differ in how the preprocessor searches for the file.
The angle-bracket form is for standard library and third-party library headers. The compiler searches its built-in include directories plus any paths added with -I flags on the command line.
The double-quote form is for your own project's headers. The compiler searches the directory containing the source file first, then falls back to the system search list. Most teams use double quotes for in-project headers and angle brackets for everything else.
A practical example with a small e-commerce header:
When g++ main.cpp runs, the preprocessor expands #include "cart.h" by copying the entire content of cart.h into main.cpp at that point. It then expands the #include <string> and #include <vector> lines inside cart.h the same way, recursively. The compiler eventually sees a single, very large file containing the standard library headers followed by the CartItem and Cart declarations, followed by main.
The textual inclusion has consequences. Whatever is in a header becomes part of every file that includes it. If a header defines a function (not just declares it), every translation unit that includes the header gets its own copy of the function definition, and the linker complains about multiple definitions. The classic fix is to declare functions in the header and define them in a .cpp file, or to mark inline functions with inline.
The textual inclusion also explains why header order can matter. If a.h uses something from b.h but doesn't include it, then a source file that includes them in the wrong order won't compile. A well-written header includes everything it needs and nothing more, so order at the call site doesn't matter.
#define creates a macro: a name the preprocessor will substitute wherever it appears.
Object-like macros bind a name to a replacement token sequence:
Anywhere the preprocessor sees MAX_CART_ITEMS after the #define, it replaces it with 100. The compiler only ever sees the expanded text, so int cart[MAX_CART_ITEMS]; reaches the parser as int cart[100];. There's no type, no storage, no symbol the debugger can show. The name is gone before the compiler runs.
Function-like macros take parameters:
A call like SQUARE(5) expands to ((5) * (5)) by textual substitution. The parentheses around each parameter and around the whole body are not optional. Without them, operator precedence at the call site can scramble the meaning of the macro. SQUARE(2 + 3) with #define SQUARE(x) x * x expands to 2 + 3 * 2 + 3, which evaluates to 11 instead of the expected 25.
Macros have a second well-known hazard: they evaluate their arguments however many times the body references them. MIN(next_id(), 5) calls next_id() once for the comparison and again for the chosen branch, doubling any side effects.
Macros have many more details: the # and ## operators, variadic macros, and a long list of subtle pitfalls. For the rest of this chapter, treat #define as a way to introduce a name the preprocessor will recognize, mostly used for object-like constants and for the conditional compilation flags covered below.
#undef removes a macro definition. After #undef NAME, the preprocessor stops substituting NAME until another #define NAME reintroduces it.
Output:
#undef of a name that isn't currently defined is harmless. The directive is a no-op in that case, so you can use it defensively without checking first.
The most common use of #undef is cleaning up after a third-party header that pollutes the macro namespace. The Windows SDK defines min and max as macros, which collides with std::min and std::max and causes compile errors in any code that uses the standard versions. The conventional fix is:
A cleaner alternative is to define NOMINMAX before including Windows headers, but plenty of legacy code uses the explicit #undef pattern.
Conditional compilation directives let the preprocessor include or skip whole blocks of source based on build-time conditions. They are the mechanism behind platform-specific code, debug-vs-release builds, feature flags, and ABI versioning.
The directive family:
| Directive | Means |
|---|---|
#if expr | Include the following block if expr evaluates to non-zero |
#ifdef NAME | Include if NAME is currently defined as a macro |
#ifndef NAME | Include if NAME is NOT currently defined |
#elif expr | Else-if branch for an #if chain |
#else | Else branch for an #if chain |
#endif | Closes the chain |
Every #if, #ifdef, or #ifndef must have a matching #endif. Nested conditionals are allowed; each #endif closes the nearest open conditional.
#ifdef NAME is true if NAME is currently defined as a macro, regardless of its value. #ifndef is the negation.
Output:
If the #define ENABLE_DEBUG_LOG line is removed (or commented out), the preprocessor drops the debug line entirely. The compiler never sees it, so there's no runtime cost and no chance of someone accidentally enabling it.
A common pattern is defining the flag on the command line instead of in the source:
The -D flag is equivalent to a #define ENABLE_DEBUG_LOG at the top of every file. This is how build systems toggle debug logging, optional features, and platform-specific code paths without modifying the source.
#if takes an integer constant expression. Unlike #ifdef, it cares about a macro's value, not just whether it's defined.
Output:
The expression can use the usual arithmetic and comparison operators, and it can reference other macros. An undefined macro inside an #if expression evaluates to 0, which means #if UNKNOWN_FLAG is the same as #if 0. Misspelling a flag name gives no error, so the block is simply skipped with no warning.
For checking whether a name is defined inside an #if expression, use defined(NAME):
defined(NAME) evaluates to 1 if the name is currently a macro, 0 otherwise. It's the only way to test "is this defined?" inside a compound #if expression with &&, ||, or !.
#elif chains additional conditions onto an #if. #else is the fall-through.
The preprocessor walks the chain top to bottom, selecting the first true branch and discarding the rest. Only one branch survives into the translation unit. The discarded branches don't need to be valid C++; they're not parsed, just skipped as text.
This last point is important. Code inside a discarded #if branch can contain syntax errors, undefined functions, or references to nonexistent headers, and the program still compiles. Only the chosen branch gets parsed:
Compilers do still scan discarded blocks for matching #if/#endif pairs, so unbalanced conditionals inside a #if 0 block do produce errors. Beyond that, the text is invisible to the parser.
A header that's included more than once in the same translation unit causes problems. If the header declares a type, the type gets declared twice, which is sometimes legal but usually triggers redefinition errors. If the header defines an inline function with a class member, that's a hard redefinition.
Multiple inclusion happens easily. Two source files might each include cart.h. That's fine, since each is its own translation unit. But within a single source file, if cart.h includes customer.h, and the source also explicitly includes customer.h, then customer.h ends up in the translation unit twice.
The classic solution is the include guard: an #ifndef/#define/#endif wrapper around the entire header.
The first time customer.h is included, CUSTOMER_H isn't defined. The #ifndef is true, so the preprocessor includes the body, and the #define CUSTOMER_H line runs, marking the guard as set. On every subsequent inclusion in the same translation unit, CUSTOMER_H is already defined, the #ifndef is false, and the entire header body is skipped.
The guard name must be unique across the whole project. The convention is the file name in all caps with underscores, like CART_H for cart.h or ORDER_TRACKING_H for order_tracking.h. Project-wide prefixes like MYPROJECT_CART_H are common in large codebases where short names might collide with library headers.
Guards prevent inclusion within a single translation unit. They don't prevent the textual content from appearing in many different translation units, which is fine: that's how headers are supposed to work. Each .cpp file gets its own copy.
#pragma is the escape hatch for instructions the C++ standard doesn't define. Each compiler can ship its own pragmas, and what they do is implementation-defined. An unrecognized pragma is ignored without error, which makes pragmas portable in the sense that the file still compiles on a compiler that doesn't know them, just without the intended effect.
The most common pragma is #pragma once, an alternative to include guards.
The pragma tells the compiler to include this header at most once per translation unit, regardless of how many times the file is referenced. No macro guard name to invent, no #endif to forget at the bottom.
#pragma once is supported by GCC, Clang, MSVC, and almost every modern C++ compiler, but it's not part of the C++ standard. The implementation typically tracks the file's inode (on Unix) or canonical path (on Windows) and refuses to include it more than once. Edge cases involving symlinks, network mounts, or duplicate-copy files in different paths can confuse this tracking. For a single repository on local disk, #pragma once is reliable.
The pragmatic choice: prefer #pragma once for new code because it's shorter and unforgettable, but be ready to fall back to traditional #ifndef guards when targeting unusual platforms or when a project's style guide mandates them. Some projects use both, belt-and-suspenders style.
#pragma pack controls structure alignment. By default, the compiler inserts padding bytes between struct members so each member sits on a natural boundary for its type. #pragma pack(1) disables padding, packing members back-to-back.
Output:
Without packing, the int after char status gets 3 bytes of padding so it lands on a 4-byte boundary, and the struct ends up 16 bytes. With #pragma pack(1), the members sit at offsets 0, 1, and 5, totaling 13 bytes. The push/pop form saves and restores the previous packing value, so the pragma only affects the structs between them.
Packing is useful when defining types that match an external binary format: network protocol headers, file format records, or hardware register layouts. It's harmful as a general optimization. Reading or writing a misaligned int is slower on most CPUs and is undefined behavior on architectures that require natural alignment.
A few other pragmas show up frequently:
#pragma GCC diagnostic ignored "-Wunused" (GCC, Clang): temporarily silence a specific warning for a block of code. Always pair with #pragma GCC diagnostic push and #pragma GCC diagnostic pop to limit the scope.#pragma warning(disable: 4996) (MSVC): the MSVC equivalent. Used to silence "deprecated" warnings on POSIX functions like strncpy.#pragma omp parallel for (OpenMP): turn a for loop into a parallel loop. The pragma is the OpenMP API itself.Because pragmas vary by compiler, code that uses them often has to wrap them in #ifdef checks for the compiler in question. This is one of the few places where conditional compilation on __GNUC__, _MSC_VER, or __clang__ is the conventional answer.
#error aborts the compile with a custom message. #warning (a GCC/Clang extension that became standard in C++23) prints a warning without stopping.
These directives are checked during preprocessing, so the message appears before the compiler even tries to parse the program. Catching misconfigurations this early gives a clear human-readable explanation, rather than letting them produce a confusing template error message a few seconds later.
A practical use is enforcing a minimum standard version. The predefined macro __cplusplus expands to a value that encodes the C++ standard version: 201103L for C++11, 201402L for C++14, 201703L for C++17, 202002L for C++20. A header that needs C++17 features can fail the compile cleanly:
Without the #error, the user gets a confusing compile error about std::optional or if constexpr, with no hint that the cause is the standard version. With it, the failure is immediate and self-explanatory.
#error is also useful as an assertion that an #if/#elif chain is exhaustive:
Reaching the #else branch means the user forgot to set the platform flag, and the build fails with a helpful pointer.
#line changes what the compiler reports for the current line number and (optionally) file name. The compiler uses these for diagnostic messages, the __LINE__ and __FILE__ predefined macros, and debugger info.
Direct uses of #line in handwritten C++ are rare. The directive exists for tools that generate C++ source from other formats: code generators, Yacc/Bison parsers, embedded DSL preprocessors, template instantiators. When such a tool emits C++ code derived from a .y file or a .ttl template, it includes #line directives pointing back at the original source so error messages reference the file the developer actually wrote, not the generated intermediate.
For ordinary C++ source, #line is one of those features that exists, has a real purpose, and rarely appears in hand-written code.
The preprocessor maintains a small set of predefined macros that capture information about the source location and the build environment. These are heavily used inside other macros, especially logging and assertion macros.
| Macro | Expands to |
|---|---|
__FILE__ | Current source file name as a string literal |
__LINE__ | Current line number as an integer |
__DATE__ | Compile date, format "Mmm dd yyyy" |
__TIME__ | Compile time, format "hh:mm:ss" |
__cplusplus | C++ standard version (201103L for C++11, etc.) |
__func__ | Current function name (not strictly a macro, but acts like one) |
A typical use combines __FILE__ and __LINE__ in an assertion macro:
Output:
When the assertion fires, the file and line in the message point at the user's call site, not at the macro definition. This is why the macro uses __FILE__ and __LINE__ instead of hard-coding them: each expansion captures the location of the caller.
Compilers also predefine macros that identify themselves and the target platform:
__GNUC__ is defined when compiling with GCC.__clang__ is defined when compiling with Clang._MSC_VER is defined when compiling with MSVC.__linux__, __APPLE__, _WIN32 identify the target OS.Code that needs to do something different per compiler or per platform tests these:
This kind of cross-compiler shim is one of the few legitimate uses of #define and conditional compilation in modern C++ code.
The preprocessor was the only tool for compile-time programming in early C and early C++. Constants needed #define. Inline functions needed function-like macros. Conditional compilation was the only way to specialize code per build. None of those uses fit cleanly into the C++ type system, because the preprocessor doesn't understand types at all.
Modern C++ has language features that do the same jobs while remaining inside the type system:
| Old (Preprocessor) | Modern C++ | Why Modern Wins |
|---|---|---|
#define MAX_ITEMS 100 | constexpr int MAX_ITEMS = 100; | Typed, scoped, debuggable, has an address |
#define SQUARE(x) ((x)*(x)) | constexpr int square(int x) { return x * x; } | Type-checked, single evaluation, no parens trap |
#define STATUS_OK 0 ... | enum class Status { Ok, Failed }; | Scoped, type-safe, no integer collisions |
#if DEBUG_BUILD ... #endif | if constexpr (DEBUG_BUILD) { ... } | Inside the language, sees template parameters |
#define STR(x) #x | Sometimes still needed | One of the cases where macros are unavoidable |
constexpr constants and inline functions are visible to the compiler. They have types, addresses, scopes, and debug symbols. They participate in overload resolution and template argument deduction. None of that is true of macros, which are gone before the compiler runs.
The shift looks like this for a small constant:
Both produce the same machine code for int cart[MAX_CART_ITEMS];. The constexpr version is type-checked (MAX_CART_ITEMS is an int, not a double), scoped (it can live inside a namespace or class), and visible to tools (a debugger sees it as a real variable). The macro is text, has no type, ignores scope, and the debugger can't show it.
For small functions, an inline function or a constexpr template gives the same performance as a function-like macro without the textual-substitution hazards:
The macro called as DOUBLE(next_id()) evaluates next_id() twice. The template called as doubled(next_id()) evaluates it once, binds the result to a parameter, and uses the parameter twice in the body.
Where the preprocessor remains the only answer:
__FILE__ and __LINE__ are values the compiler can't expose through any non-macro mechanism. Assertion, logging, and contract macros depend on this.-DENABLE_FEATURE_X and #ifdef ENABLE_FEATURE_X are how feature flags, platform selection, and debug-vs-release knobs travel from the build system into the source.__GNUC__ vs _MSC_VER and selecting between __attribute__((always_inline)) and __forceinline is a preprocessor job. The language has no portable way to express "use this attribute when compiling with GCC."TEST, EXPECT_EQ) because they need to register tests at static-init time and capture failure source locations. The macro form is part of the framework's public API.Outside of those cases, prefer constexpr first, inline second, enum class third, and template fourth. Macros come last, when nothing else fits.
10 quizzes