thecodex.expert · The Codex Family of Knowledge
C++

Preprocessor and Macros

A macro has no type checking, no scoping, and no debugger visibility — constexpr and templates give C++ genuinely better alternatives for nearly everything macros used to be needed for.

C++20 / C++23 Prefer constexpr/templates over macros Last verified:
Canonical Definition

C++'s preprocessor is identical in mechanism to C's — a pure text-substitution pass running before real compilation, handling #include, #define, and conditional compilation directives. What's different is C++'s much richer set of alternatives: constexpr functions and variables are evaluated at compile time with full type checking (unlike a macro, which has none), and templates provide genuine generic programming with type safety a macro can't offer. Modern C++ guidance is explicit: use a macro only for the handful of things nothing else can do (include guards, conditional compilation, stringification), and reach for constexpr or a template for everything else.

constexpr replaces the classic "constant" macro

Unlike #define MAX_SIZE 100 (pure text substitution, no type, invisible to a debugger), constexpr gives MAX_SIZE a real type, real scoping, and full debugger visibility — while still being usable anywhere a compile-time constant is required, like an array size.

C++constexpr_vs_macro.cpp
#define MAX_SIZE 100         // old style: no type, no scope, invisible to a debugger

constexpr int MAX_SIZE2 = 100;   // modern: a real, typed, scoped constant

int arr[MAX_SIZE2];   // usable at compile time, just like the macro version

Templates replace the classic "generic" macro

The macro version has zero type checking — SQUARE("hello") would compile and produce nonsense; the template version is fully type-checked, and the compiler will correctly reject a call that doesn't make sense for the given type.

C++template_vs_macro.cpp
#define SQUARE(x) ((x) * (x))   // old style: no type checking at all

template <typename T>
T square(T x) { return x * x; }   // modern: fully type-checked, works for any multipliable T

square(5);      // 25
square(2.5);    // 6.25

What macros are still genuinely needed for

Include guards, conditional compilation for platform-specific code, and stringification (turning a token into a literal string with #) remain things only the preprocessor can do — these legitimate, narrow uses are why the preprocessor hasn't been removed from the language despite modern C++'s general preference against macros.

C++legitimate_macro_uses.cpp
#ifndef MYHEADER_H   // include guard — still the standard approach
#define MYHEADER_H
// ...
#endif

#define STRINGIFY(x) #x
std::cout << STRINGIFY(hello) << "\n";   // prints "hello" — turns the token into a string literal

Sources

1
ISO/IEC 14882 (C++ Standard), "Preprocessor," cppreference.com/w/cpp/preprocessor.