How C++ Preprocessor Handles Recursive Macros

In the C++ compilation pipeline, the preprocessor prevents infinite compilation loops by explicitly disabling recursive macro expansion through a mechanism commonly referred to as macro "painting" or token marking. When a macro name is recognized and replaced, that specific macro is marked as disabled for the duration of the scan and rescan of its replacement list. Consequently, self-referential or mutually recursive macros do not expand infinitely; instead, any nested occurrence of an active macro is left unchanged as a literal token in the output token stream.

The Role of Macro Expansion in Translation Phase 4

The C++ compilation process is divided into distinct translation phases. Macro replacement occurs during Translation Phase 4, well before semantic analysis, type checking, or code generation take place. The preprocessor operates strictly on preprocessor tokens. Because it does not possess an execution stack or a dynamic evaluation engine, handling unbounded recursion without a safeguard would cause the compiler to run indefinitely or crash due to resource exhaustion.

The Macro Disabling Mechanism ("Painting Blue")

To prevent infinite expansion loops, the ISO C++ standard specifies an exact algorithm for rescanning replacement lists. When a macro is expanded:

  1. Identification and Replacement: The preprocessor identifies a macro name (and its arguments, if it is a function-like macro) and substitutes it with its replacement list.
  2. Disabling Current Identifiers: The name of the macro currently being expanded is marked as "disabled" (historically termed "painted blue" in compiler design literature) within the context of that replacement list.
  3. Rescanning: The resulting token sequence—including any adjacent tokens if concatenation (##) occurred—is rescanned from left to right for further macro names to expand.
  4. Suppression of Recursive Calls: If the preprocessor encounters a disabled macro identifier during this rescan, it treats the identifier as a standard, unexpandable token and leaves it untouched.
  5. Re-enabling: Once the preprocessor completes the expansion of that macro context and moves past the resulting tokens, the macro name is re-enabled for subsequent independent occurrences in the source file.

Direct vs. Indirect Recursion

The preprocessor applies this suppression logic to both direct and indirect (mutual) recursion.

Direct Recursion

In direct recursion, a macro refers directly to itself:

#define VALUE (1 + VALUE)
int x = VALUE;

During preprocessing:

  1. VALUE is replaced by (1 + VALUE).

  2. The inner VALUE is marked disabled.

  3. The replacement list is rescanned. The inner VALUE is recognized, but because it is disabled, it is not expanded.

  4. The output emitted to Phase 5 is:

    int x = (1 + VALUE);

Indirect Recursion

In mutual or indirect recursion, two or more macros reference each other cyclically:

#define A B
#define B A
int y = A;

During preprocessing:

  1. A expands to B, and A is added to the disabled set.

  2. B is scanned and expands to A, adding B to the disabled set alongside A.

  3. The resulting token A is rescanned. Because A is currently marked as disabled, expansion ceases.

  4. The output emitted to Phase 5 is:

    int y = A;

Implications for Preprocessor Metaprogramming

Because true recursion is strictly forbidden by the language standard, libraries that perform advanced preprocessor metaprogramming (such as Boost.Preprocessor) cannot use traditional recursive calls. Instead, they simulate repetition and looping structures through techniques such as:

  • Table-based evaluation: Defining individual macros for fixed iteration depths (e.g., REPEAT_1, REPEAT_2, ..., REPEAT_256).
  • Deferred expansion: Using indirect evaluation macros (empty tokens inserted between macro names and argument lists) to delay the binding of arguments, allowing rescanning passes to re-trigger evaluation after a macro leaves its disabled state.

By enforcing the disabled token rule during rescanning, the C++ preprocessor guarantees that translation phase 4 always terminates, ensuring predictable behavior and deterministic build times across all standard-compliant toolchains.