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.
#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 versionTemplates 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.
#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.25What 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.
#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