AlgoMaster Logo

Preprocessor & Macros

Low Priority21 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

Before the C++ compiler sees a single line of code, a different program runs over the source file and rewrites it. That program is the preprocessor, and the directives that drive it (#include, #define, #undef, and the conditional family) operate as pure text substitution, with no awareness of types, scopes, or even C++ syntax. This chapter covers what the preprocessor does, how #define builds object-like and function-like macros, the stringize (#) and token-paste (##) operators, the classic macro pitfalls, and why modern C++ replaces most macros with constexpr, inline, and enum class.

The Preprocessor Is a Separate Pass

Running g++ shop.cpp -o shop looks like one step, but the toolchain is a small pipeline. The preprocessor runs first, takes the .cpp file plus everything it pulls in via #include, expands every macro, evaluates every conditional directive, and produces a single flat translation unit as plain text. That text is what the compiler sees. Only after the preprocessor finishes do the parser, type checker, and code generator get involved.

Each box is a real program on a typical Linux install: cpp does preprocessing, cc1plus is the C++ compiler proper, as turns assembly into object files, and ld links the pieces together. To stop after the first stage and see what the preprocessor handed downstream, pass -E to g++:

Running g++ -E shop.cpp prints the post-preprocessor text. The body of main becomes int cart[100]; because the preprocessor walked through the file and replaced every occurrence of the token MAX_CART_ITEMS with 100. The compiler never sees the name MAX_CART_ITEMS at all. This detail explains many of the surprises macros produce.

A few properties fall out of this design:

  • The preprocessor operates on tokens, not on the syntax tree. It doesn't understand expressions, declarations, or scopes.
  • It runs before the compiler, so macros have no type, no namespace, and no respect for {} blocks.
  • Its output is text. If a macro produces nonsense, the compiler reports the error on the expanded text, which can look different from what was written.

Object-Like Macros with #define

The simplest form of #define binds a name to a replacement token sequence. Anywhere the preprocessor sees the name (outside string literals and comments), it substitutes the replacement.

Read each #define as a rewrite rule. MAX_CART_ITEMS -> 100, DEFAULT_CURRENCY -> "USD", and so on. The replacement runs through the rest of the file (or until an #undef), in every context where the name appears as a standalone token.

A few common gotchas:

  • There's no = between the name and the value. #define MAX_CART_ITEMS = 100 would expand MAX_CART_ITEMS to = 100, which is rarely useful.
  • There's no semicolon at the end. #define MAX_CART_ITEMS 100; makes int cart[MAX_CART_ITEMS]; expand to int cart[100;];, a syntax error.
  • The replacement is everything from after the name to the end of the line (with line continuations via \). Whitespace before and after gets trimmed, but quoted strings stay intact.
  • Macro names are conventionally ALL_CAPS_WITH_UNDERSCORES. This isn't enforced by the compiler, but it's the universal convention, and breaking it gets noticed in code review.

Because the preprocessor is text substitution, a macro has no type. MAX_CART_ITEMS isn't an int; it's the literal string 100 that the compiler will later parse as an integer literal. Taking its address fails:

The error talks about &100, not about MAX_CART_ITEMS. The macro is gone by the time the compiler complains. This is one of several reasons modern code prefers constexpr for named constants, covered later.

Function-Like Macros

Macros can take parameters. The syntax looks like a function definition, but the semantics are still pure text substitution.

The call MAX(price1, price2) is rewritten to ((price1) > (price2) ? (price1) : (price2)) before the compiler sees anything. There's no function call at runtime, no stack frame, no argument copying. The preprocessor pastes the arguments into the body.

The parentheses look excessive. They're not. Every argument is wrapped in parens, and the whole body is wrapped in one more pair. Without those parens, two classic macro bugs (precedence and side-effect bugs) appear.

Function-like macros also accept variable arguments via ... and __VA_ARGS__, which makes printf-style logging macros possible:

The backslash at the end of the first line continues the macro definition onto the next physical line. The preprocessor stitches them together before substitution. __VA_ARGS__ expands to the comma-separated list of arguments that matched .... Adjacent string literals ("[order] " format "\n") are concatenated by the compiler, not the preprocessor.

The Missing-Parens Trap

Function-like macros look like functions, but they don't behave like functions, and the most common bug comes from forgetting that. A version of SQUARE that drops the inner parens:

What's wrong with this code?

Expected 25, because (2 + 3) * (2 + 3) == 25. The output is 11. The macro expands by pasting the argument text in place, so SQUARE(2 + 3) becomes 2 + 3 * 2 + 3. Operator precedence kicks in: 3 * 2 runs first, then the additions, giving 2 + 6 + 3 = 11.

Fix: Wrap every parameter use in parentheses, and wrap the entire body in one more pair:

Now SQUARE(2 + 3) expands to ((2 + 3) * (2 + 3)), which evaluates to 25 regardless of what surrounds the call site.

The outer parens matter too. 10 / SQUARE(2) with #define SQUARE(x) (x) * (x) expands to 10 / (2) * (2), which is 10, not 2.5. Wrapping the whole body in parens fixes that: ((2) * (2)) becomes a single grouped expression.

The rule is mechanical and unforgiving: every parameter in parens, every body in parens, no exceptions. It looks ugly. The alternative is bugs that survive review and only appear at runtime when someone passes an expression instead of a single token.

The Multiple-Evaluation Trap

Even with perfect parens, function-like macros have a second hazard: they evaluate their arguments more than once.

What's wrong with this code?

Reading MAX as a function suggests next_id() runs once and the result is compared against 1005. It doesn't. The macro expands to:

next_id() runs once for the comparison, then runs again if the comparison picks the first branch, returning a different value the second time. The first call returns 1000 (so 1000 > 1005 is false, and the 1005 branch is taken), printing 1005. Worse, the side effect of incrementing id happens twice when the first branch is taken, and once when the second is. The behavior depends on which branch the ternary picks, which depends on the value the call returned.

Fix: Use a real function (or std::max):

std::max evaluates each argument exactly once before the comparison. Any function-like macro that uses an argument more than once in its body has this hazard, and it can't be fixed inside the macro: the argument text is what gets pasted, and pasting it twice means evaluating it twice.

Function-like macros look "free" because there's no call overhead, but multiple-evaluation can double or triple the work of a non-trivial argument. An inline function gives the same zero call overhead without the trap.

No Scope, No Types

Macros live in a separate namespace (or rather, no namespace at all) from C++ names. They don't respect {} blocks, classes, namespaces, or templates. Once defined, a macro is active until an #undef or the end of the translation unit.

This doesn't compile. The preprocessor finds DISCOUNT inside namespace shop and substitutes 10, producing int 10 = 25;, which makes no sense as a declaration. The namespace doesn't protect anything; the preprocessor doesn't know namespaces exist.

The same problem appears with member names, parameter names, and even strings of source code that share a name with a macro. Windows headers famously define min and max as macros, which collides with std::min and std::max. The workaround is #define NOMINMAX before including the Windows headers, or wrapping calls in parens: (std::min)(a, b) so the preprocessor doesn't see min as a macro name.

Macros also have no type. They can't be overloaded, passed to a template, have their address taken, or put in a header behind a class:

The class member LIMIT is an int, lives inside Cart, and can be referenced as Cart::LIMIT. The macro is a free-floating text rule with none of those properties.

Stringizing with #

Inside a function-like macro body, prefixing a parameter with # turns its argument into a string literal. This is the stringize operator. The preprocessor takes the text of the argument, adds quotes, and escapes any embedded \ or " characters.

LABEL(cart_total) expands to "cart_total", with the quotes added by the preprocessor. LABEL(2 + 3) expands to "2 + 3", preserving whitespace as it appeared in the source. The argument is never evaluated as an expression: the macro sees its text, not its value.

A common use is an assertion-like macro that prints the failed condition:

If cartItems were 0, the macro would print Requirement failed: cartItems > 0 and exit. The #cond inside the body grabs the literal source text of the condition and turns it into a string, useful for error messages because the condition expression can't usually print itself.

The do { ... } while (0) wrapper is a classic macro idiom. It makes the macro behave like a single statement that requires a trailing semicolon, even when used inside an if/else without braces, without dangling-else surprises.

Token-Pasting with ##

The token-paste operator ## joins two tokens into one during macro expansion. It's the tool for building identifier names from pieces.

price_##name pastes the literal text price_ onto whatever name expands to, producing a single identifier price_mouse or price_keyboard. The result has to be a valid token, so pasting something the lexer can't parse (e.g., 1 and +) won't produce a usable token.

A useful pattern combines # and ## to generate logging keys:

The #category and #name build the human-readable key "cart.size", while metric_##category##_##name builds the variable name metric_cart_size. The same call site produces both a string label and an identifier reference.

Token pasting is the only way to programmatically generate names in C++, but heavy use of ## is a sign that a template, a std::map<std::string, int>, or a code-generation step would fit better.

#undef and Macro Lifetime

A macro stays defined from its #define to the end of the translation unit, unless an #undef removes it.

Redefining a macro to the same replacement is fine. Redefining it to a different replacement without an #undef first is technically an error, though most compilers downgrade it to a warning. #undef of a name that isn't currently defined is harmless and does nothing.

#undef undoes a macro from a third-party header that conflicts with your code. Include the offending header, immediately #undef the offending name, then continue. The min/max Windows macros are the canonical example.

Why Modern C++ Prefers Constexpr, Inline, and Enum Class

For most cases where macros showed up in older C++ code, modern C++ has a better answer. Macros lose type information, ignore scope, and play tricks during preprocessing that the rest of the language doesn't get to see. The alternatives keep all of that, and they cost nothing at runtime.

Use CaseOld (Macro)Modern C++Why Modern Wins
Named integer constant#define MAX_CART_ITEMS 100constexpr int MAX_CART_ITEMS = 100;Typed, scoped, debuggable, can have its address taken
Named string constant#define DEFAULT_CURRENCY "USD"constexpr const char* DEFAULT_CURRENCY = "USD";Same plus actual const char* type
Small function#define SQUARE(x) ((x) * (x))inline int square(int x) { return x * x; }Type-checked, single evaluation, no parens trap
Min / max#define MAX(a, b) ...std::max(a, b)Works on any comparable type, no double-eval
Set of named values#define STATUS_PLACED 0enum class Status { Placed, Shipped };Scoped, type-safe, no accidental conversion to int
Compile-time conditional valueMacro with #ifif constexpr (cond) { ... }Inside the language, sees template parameters

The same MAX written three ways. The macro version is what appears in C code from twenty years ago. The inline version is what most C++ programmers wrote between 1998 and 2011. The constexpr template is the modern form.

The inline int max_i(int, int) is restricted to int arguments. The constexpr template version works for int, double, std::string, anything with operator>. Both versions evaluate each argument exactly once, both respect scope, both can be debugged, both produce the same machine code as the macro after the optimizer runs.

For named constants, the comparison is even more lopsided:

None of that works with #define MAX_CART_ITEMS_OLD 100. Taking the address of the macro doesn't compile, type information is lost the moment the preprocessor expands the name, and a debugger watching the program won't show MAX_CART_ITEMS_OLD as a symbol because there's no symbol to show.

For named sets of values, enum class replaces the worst pattern macros used to fill:

The enum class rejects raw integers, requires the OrderStatus:: prefix at use sites, and lets the compiler catch typos like OrderStatus::Sipped. The macro version compiles STATUS_SIPPED as an undefined identifier with no way to tell the typo wasn't intentional.

constexpr constants and inline functions don't add any runtime cost compared to macros. The compiler folds them away at the same point in the pipeline. The macro's only "advantage" is being invisible to the type system, which is the same as saying it's untrustworthy.

When Macros Are Still the Right Answer

After all of the warnings, there are still places where macros are the right tool, because the alternatives can't express what's needed.

  • Capturing source location at the call site. The preprocessor knows which file and line a macro is invoked from. Asserts, logging, and contract macros use this so the message points to the user's code, not the library. Predefined macros like __LINE__ and __FILE__ make this work, and they get their own lesson next.
  • Header-only generic wrappers that can't be templates. Rare, but it happens. To wrap a block of code with setup and teardown that must happen lexically around the user's code (e.g., a scoped lock pattern that pre-dates RAII, or a SCOPED_TRACE-style debug helper), a macro is the only thing that can wrap arbitrary statements.
  • Build-time configuration via `-D` flags. Compiling with g++ -DENABLE_DEBUG_LOG ... defines ENABLE_DEBUG_LOG as a macro. Code can check for that with #ifdef ENABLE_DEBUG_LOG. This is how feature flags, debug-vs-release knobs, and platform selection all work.
  • Test framework boilerplate. Frameworks like Google Test rely on macros (TEST, EXPECT_EQ) because they need to register tests at static-init time and capture failure source locations. The macro form is part of the framework's API.

Outside of those, reach for constexpr first, inline second, enum class third, and template fourth. Macros come last, after ruling out everything else.

Quiz

Macros Quiz

10 quizzes