CppPreprocessor & Macros

Preprocessor & Macros

Before your compiler ever looks at C++ grammar or types, another program runs first: the preprocessor. It performs a purely textual pass over your source file, following directives that start with #, and produces a modified block of text that is what actually gets compiled. Understanding what it does — and its limits — explains a lot of C++'s stranger corners.
#include, revisited
You've already used the most common preprocessor directive constantly: #include. It literally copies the contents of another file in, textually, at that exact point.

include-forms.cpp

CPP
#include <iostream>  // angle brackets: search system/standard library paths
#include "myheader.h" // quotes: search relative to the current file first
#define — text substitution macros
#define creates a macro: a name that the preprocessor replaces with some replacement text, everywhere it appears, before compilation even starts.

define-basics.cpp

CPP
#define PI 3.14159
#define MAX_USERS 100

double area(double radius) {
  return PI * radius * radius; // becomes "3.14159 * radius * radius"
}
The classic macro bug: missing parentheses
Macros have no type safety, no scoping, and can surprise you
Because a macro is a blind text substitution, function-like macros need to parenthesize every use of their parameters — and even the whole expression — or the substituted text can be parsed completely differently than intended.

square-bug.cpp

CPP
#define SQUARE(x) x*x

int main() {
  int result = SQUARE(1 + 2);
  // The preprocessor expands this literally to: 1 + 2*1 + 2
  // which evaluates to 1 + 2 + 2 = 5, NOT 9 as you'd expect!
  return result;
}
The fix is to wrap both the parameter and the whole macro body in parentheses: #define SQUARE(x) ((x)*(x)). That expands SQUARE(1 + 2) to ((1 + 2)*(1 + 2)), which correctly evaluates to 9. But even fixed, the macro still has no idea what type x is, won't respect namespaces or scoping, and will silently evaluate its argument twice — which matters if the argument has side effects. This is exactly why modern C++ steers away from macros for anything beyond simple constants.
Header guards
If a header file gets #included more than once in the same translation unit (easy to happen indirectly, through other headers), its contents would be duplicated — leading to "redefinition" compiler errors. A header guard prevents this using conditional compilation.

myheader.h

CPP
#ifndef MYHEADER_H
#define MYHEADER_H

struct Point {
  double x;
  double y;
};

#endif // MYHEADER_H
The first time this header is included, MYHEADER_H is undefined, so the body runs and defines it. Any later include in the same file sees MYHEADER_H already defined and skips the body entirely, avoiding the duplicate definition.
#pragma once
A simpler, near-universal alternative
Nearly every modern compiler also supports #pragma once, placed once at the top of a header, which achieves the same effect with a single line and no risk of a typo in the guard macro name colliding with another header. It isn't part of the official C++ standard, but it's supported everywhere in practice, and most codebases prefer it for its simplicity.

myheader-pragma.h

CPP
#pragma once

struct Point {
  double x;
  double y;
};
Conditional compilation
#ifdef, #ifndef, and #if let you compile different code depending on which macros are defined — commonly used for platform-specific code or debug-only logging.

conditional-compilation.cpp

CPP
#include <iostream>

#define DEBUG_MODE 1

int main() {
#ifdef DEBUG_MODE
  std::cout << "Debug build: extra logging enabled\n";
#endif

#if DEBUG_MODE == 1
  std::cout << "Verbose diagnostics on\n";
#else
  std::cout << "Release mode\n";
#endif

  return 0;
}
Why modern C++ avoids macros

Almost everything macros were historically used for now has a safer, type-checked replacement built into the language itself:

  • const / constexpr for named constants — typed, scoped, and debuggable, unlike #define PI 3.14159.

  • inline functions instead of function-like macros — arguments are evaluated once, types are checked, and you can step through them in a debugger.

  • Templates for generic code that needs to work across types — macros cannot understand types at all.

  • Reserve #define for the cases language features cannot replace, like header guards or conditional compilation, and prefer #pragma once for the header-guard case.