C++: Preprocessor Directives

Understand how the preprocessor transforms your source code before compilation using #include, #define, conditional directives, and predefined macros.

What is the Preprocessor?

The preprocessor is a text-transformation tool that runs before the compiler sees your code. It reads your source file, follows every directive that starts with #, and produces a modified file that the compiler then compiles. Preprocessor directives are not C++ statements: they have no semicolons, operate on text only, and are resolved entirely before type checking begins.

Preprocessor at a Glance

The build pipeline is: preprocessor first, then compiler, then linker. The preprocessor works on raw text with no knowledge of C++ types or syntax.

  • Every directive starts with # and occupies its own line
  • The preprocessor only manipulates text: it copies, replaces, or removes lines
  • No type checking: that is the compiler's job, after the preprocessor finishes
  • Common tasks: including headers, defining constants, enabling debug-only code

#include

#include copies the entire contents of the named file into the current file at the point of the directive. Angle brackets search the standard library directories; double quotes search the project directory first, then the system directories.

#include

Think of #include as a copy-paste: the preprocessor literally pastes the header's text into your file before compilation.

  • #include <header>: system or standard library header
  • #include "header.h": project-local header file
  • Including the same header twice in one file can cause redefinition errors, solved by include guards or #pragma once
  • Heavy use of #include is one reason large C++ projects can be slow to compile

#include Standard Headers

C++

Each #include pastes the full header into this file. The compiler then sees one combined translation unit.

#define: Macros for Constants

#define NAME value tells the preprocessor to replace every occurrence of NAME with value as plain text before compilation. For numeric constants, prefer const or constexpr in modern C++ because they are type-safe and visible to the debugger, while macros are not.

#define constants

Macros are dumb text substitution. They have no type, no scope, and cannot be inspected in a debugger. Prefer const or constexpr for named constants in modern C++.

  • Syntax: #define NAME value (no equals sign, no semicolon)
  • The preprocessor replaces every occurrence of NAME with value before the compiler runs
  • No type safety: #define PI 3.14 makes PI a floating-point literal with no declared type
  • Prefer: constexpr double PI = 3.14159; for the same purpose with full type safety

#define for Constants

C++

The preprocessor replaces PI, E, and APPNAME with their text values before the compiler reads the file.

Function-like Macros

A macro can take parameters, making it look like a function call. The preprocessor expands the arguments by text substitution. Because there is no type checking and no function call overhead, they were once used for performance-critical inline utilities. Modern C++ inline functions and templates do the same job more safely.

Function-like Macro

Wrap every parameter and the whole expression in parentheses to prevent operator-precedence surprises. Even then, avoid side-effectful arguments like SQUARE(x++) which evaluates x++ twice.

  • Syntax: #define SQUARE(x) ((x) * (x))
  • Parentheses around each parameter prevent precedence bugs: SQUARE(a+b) expands to ((a+b)*(a+b))
  • No type checking: SQUARE("hello") compiles and produces nonsense
  • Prefer inline functions or templates for the same purpose in modern C++

Function-like Macros

C++

Parentheses around every parameter are critical. SQUARE(2+3) becomes ((2+3)*(2+3)) = 25, not 2+3*2+3 = 11.

#undef

#undef removes a macro definition. After an #undef, the macro name is no longer recognized by the preprocessor. This is useful when you need to redefine a macro with a different value for a specific section of code.

#undef

#undef is the counterpart of #define. It clears the name so a new #define can reuse it, or so the name stops being replaced past that point.

  • Syntax: #undef NAME
  • After #undef, any subsequent use of NAME is left as-is by the preprocessor
  • Useful to temporarily redefine a macro: #undef MAX then #define MAX(a,b) ...
  • Not commonly needed in modern code, prefer const/constexpr to avoid the problem entirely

#undef and Redefine

C++

#undef clears VERSION, then a new #define gives it a fresh value. The two cout lines see different numbers.

Conditional Compilation

Conditional directives let the preprocessor include or exclude blocks of code based on whether a macro is defined or what value it holds. This is widely used to enable debug logging, switch between platforms, or activate features without modifying the source.

Conditional Directives

The preprocessor evaluates the condition and physically removes the excluded block before the compiler ever sees it. Excluded code has zero compile time and zero runtime cost.

  • #ifdef NAME: include the block if NAME is defined
  • #ifndef NAME: include the block if NAME is not defined
  • #if EXPR: include the block if the integer expression is non-zero
  • #elif / #else / #endif: same structure as if/else if/else

Conditional Compilation with DEBUG and VERSION

C++

Commenting out #define DEBUG removes the debug line from the compiled binary entirely. VERSION selects one branch at compile time.

Include Guards

When a header file is included by multiple source files, or indirectly included more than once through nested headers, its contents are pasted in multiple times. This causes redefinition errors. An include guard uses #ifndef to ensure the body of the header is compiled only once per translation unit.

Include Guard Pattern

The guard macro name should match the file path uniquely: MY_PROJECT_MATH_H for a file at my_project/math.h. Collisions between guard names in different headers can cause silent skips.

  • First inclusion: MATH_H is not defined, so the body is compiled and MATH_H is defined
  • Second inclusion: MATH_H is already defined, so the entire body is skipped
  • #pragma once is a simpler modern alternative supported by all major compilers
  • Traditional guards are portable and do not rely on compiler extensions

Include Guard in a Header File

C++

The first #include defines MATH_UTILS_H and compiles the body. The second #include sees MATH_UTILS_H already defined and skips everything.

#pragma once

#pragma once placed at the top of a header file tells the compiler to include that file only once per translation unit, exactly like a traditional include guard but with less boilerplate. It is supported by GCC, Clang, and MSVC.

#pragma once

#pragma once is the recommended modern approach for preventing double inclusion. It is shorter and eliminates the risk of a typo in the guard macro name.

  • Place #pragma once as the very first line of any header file
  • No macro name to invent or mis-spell
  • Supported by all major compilers: GCC, Clang, MSVC
  • Technically a compiler extension, not part of the ISO standard, but universally available in practice

#pragma once in a Header

C++

One line at the top replaces the three-line include guard. Duplicate includes of geometry.h are silently ignored.

Predefined Macros

The C++ standard defines several macros that the preprocessor sets automatically. They provide metadata about the source file and compilation time, which is useful for logging, assertions, and diagnostics.

Standard Predefined Macros

These macros require no #define. The preprocessor expands them automatically based on the current file, line, and build time.

  • __FILE__: the current source file path as a string literal
  • __LINE__: the current line number as an integer
  • __DATE__: the compilation date as a string, e.g. "Apr 26 2026"
  • __TIME__: the compilation time as a string, e.g. "14:30:00"
  • __cplusplus: the C++ standard version as a long integer, e.g. 201703L for C++17

Predefined Macros in Action

C++

__FILE__ and __LINE__ inside logError show where the function was defined, not where it was called. Move the macro uses to the call site for accurate location.

Platform-specific Code with Conditional Compilation

Compilers automatically define macros that identify the target platform. Conditional directives let a single source file include platform-specific code while remaining portable, the sections for other platforms are removed entirely before compilation.

Platform Detection Macros

Use these read-only compiler-defined macros to branch on the target OS or compiler. Never define them yourself.

  • _WIN32: defined on Windows (32-bit and 64-bit)
  • __linux__: defined on Linux
  • __APPLE__: defined on macOS and iOS
  • _MSC_VER: defined when using MSVC; value is the compiler version
  • __GNUC__: defined when using GCC or Clang

Platform and Standard Detection

C++

The compiler defines these macros automatically. Only the matching branch is compiled into the binary.

Knowledge Check

1. When does the C++ preprocessor run?

2. What does #include <vector> do?

3. What is the key risk of function-like macros compared to inline functions?

4. What is the purpose of an include guard?

5. Which directive checks whether a macro has NOT been defined?

6. What does #undef MY_MACRO do?

7. Which predefined macro expands to the current line number?

8. What does #pragma once do at the top of a header file?