🎩 The Preprocessor
Function-like macros: power tools with no guard
▶ Open the interactive lesson — free, no signupA macro that squares a number can quietly turn SQUARE(a + 1) into 9 instead of 25 — a fifty-year-old trap that still bites professional codebases today. This lesson shows you exactly why, then hands you the defenses: the parenthesizing rules, the do { } while (0) trick the Linux kernel swears by, and the secret behind every "print the expression AND its value" debugging helper C programmers reach for.
Add parentheses right after the macro name — no space! — and #define takes arguments, making a function-like macro: #define SQUARE(x) x * x. Now SQUARE(7) becomes 7 * 7. It looks like a function, but remember the mantra: it's still just text. Arguments aren't worked out to a value and handed over the way a function's are — they're pasted, character for character.
The classic disaster, in three acts
#include <stdio.h>
#define SQUARE(x) x * x
int main(void) {
int a = 4;
printf("%d\n", SQUARE(3)); /* fine: 3 * 3 = 9 */
printf("%d\n", SQUARE(a + 1)); /* ...uh oh */
return 0;
}🤔 SQUARE(a + 1) with a = 4 — what prints?
Think first
9, not 25! The paste is literal: SQUARE(a + 1) → a + 1 * a + 1 → 4 + (1×4) + 1 = 9. The macro never saw the value 5 — it saw the three tokens a, +, 1 and dropped them into a precedence minefield.
The repaired version parenthesizes each parameter: #define SQUARE(x) (x) * (x). But that's still only half the armor:
🤔 With #define SQUARE(x) (x) * (x), what does 100 / SQUARE(5) give?
Think first
100, not 4. Expansion: 100 / (5) * (5). Division and multiplication associate left-to-right: (100/5) × 5 = 100. The parameters were protected but the whole expression was not — it needs an outer set: ((x) * (x)).
Hence the full parenthesization rule: parentheses around every parameter use AND around the whole replacement:
/* every parameter use wrapped + the whole body wrapped */
#define SQUARE(x) ((x) * (x))
/* the same discipline, always: */
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))🧠 Checkpoint: Why must a macro body like (x) * (x) get one MORE pair of parentheses around the whole thing?
- Style guides require it
- Operators outside the macro can still tear the expression apart, e.g.
100 / SQUARE(5) - The preprocessor rejects unparenthesized bodies
- To make the macro evaluate arguments once
Show answer
Operators outside the macro can still tear the expression apart, e.g. 100 / SQUARE(5) — Inner parens protect against precedence inside the arguments; the outer pair protects against precedence at the call site. Neither can fix double evaluation — that is inherent to pasting.
And even the perfect version has one unfixable flaw — arguments that get pasted twice are evaluated twice:
🤔 int i = 4; int r = SQUARE(i++); — what are r and i afterwards?
Danger — think first
Trick question: anything at all. The expansion ((i++) * (i++)) modifies i twice with no sequence point between — that is undefined behavior per the standard. You might see r=16, i=6 today, and something else after a compiler upgrade. UB means all bets are off, not "probably 20".
Never pass expressions with side effects to a macro (i++, function calls, assignments). SQUARE(i++) expands to ((i++) * (i++)) — two unsequenced modifications of i, which is undefined behavior, not merely "incremented twice". A real function evaluates its argument exactly once; a macro makes no such promise.
Multi-statement macros: the do { } while (0) idiom
Suppose a macro needs two statements. Wrapping them in bare braces seems reasonable — until the macro meets an if:
#define SWAP(a, b) { int t = (a); (a) = (b); (b) = t; }
void order(int x, int y) {
if (x > y)
SWAP(x, y); /* expands to { ... } ; */
else /* error: 'else' without a previous 'if' */
x = y;
}Expansion of the bad version: the if owns the { ... } block, then the user's semicolon becomes an empty statement, and the else is left dangling with nothing to attach to — a syntax error (or worse, silently wrong pairing in nested ifs). The cure is a loop that runs exactly once:
#define SWAP(a, b) do { int t = (a); (a) = (b); (b) = t; } while (0)
void order(int x, int y) {
if (x > y)
SWAP(x, y); /* one statement + required ';' — perfect */
else
x = y; /* compiles, and pairs correctly */
}Why it works: do { ... } while (0) is a single statement that demands a semicolon — so SWAP(a, b); behaves exactly like a function call in every grammatical position, including between if and else. Every serious C codebase (Linux kernel included) uses this idiom.
🧠 Checkpoint: What problem does do { ... } while (0) solve in a macro?
- It makes the body run faster
- It lets the macro return a value
- It turns multiple statements into ONE statement that needs a trailing semicolon, so if/else around the macro parses correctly
- It prevents double evaluation of arguments
Show answer
It turns multiple statements into ONE statement that needs a trailing semicolon, so if/else around the macro parses correctly — A brace block plus the user’s semicolon breaks if/else pairing; do/while(0) is a single statement that eats the semicolon naturally. It does nothing about double evaluation.
The # and ## operators
Two operators exist only inside macro replacement text:
#param— stringify: turns the argument's text into a string literal.#xwithx = score+1gives"score+1".a ## b— token pasting: glues two tokens into one new token.val ## 2makes the identifierval2.
Stringify powers every debugging macro ever written — print the expression and its value without typing it twice:
#include <stdio.h>
#define DUMP(expr) printf(#expr " = %d\n", (expr))
int main(void) {
int score = 40, bonus = 2;
DUMP(score);
DUMP(score + bonus);
DUMP(score * bonus + 1);
return 0;
}$ gcc dump.c -o dump && ./dump score = 40 score + bonus = 42 score * bonus + 1 = 81 # the expression text AND its value — typed only once
Note the trick: #expr " = %d\n" works because adjacent string literals are concatenated. Token pasting shines in code generators — one macro stamping out families of declarations: DECLARE_LIST(int) can mint list_int_push, list_int_pop, and friends via list_ ## T ## _push.
Variadic macros: __VA_ARGS__
Since C99, a macro's parameter list may end in ..., and __VA_ARGS__ expands to whatever extra arguments were passed — tailor-made for wrapping printf-style functions:
#include <stdio.h>
#define LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", \
__FILE__, __LINE__, __VA_ARGS__)
int main(void) {
int users = 3;
LOG("startup ok, %d users", users);
LOG("temp=%d limit=%d", 71, 80);
return 0;
}$ gcc log.c -o log && ./log [log.c:9] startup ok, 3 users [log.c:10] temp=71 limit=80
One wrinkle: with the C99 rules, LOG("boot") — no extra args — leaves a dangling comma after __LINE__,. C23 fixes this cleanly with __VA_OPT__(x), which expands to x only if variadic arguments are present: __LINE__ __VA_OPT__(,) __VA_ARGS__. (Before C23, GCC and Clang offered , ## __VA_ARGS__ as an extension that swallows the comma.)
🧠 Checkpoint: What does C23’s __VA_OPT__(,) do in a variadic macro?
- Expands to a comma only when variadic arguments were actually passed
- Counts the variadic arguments
- Stringifies the variadic arguments
- Makes the comma operator sequence the arguments
Show answer
Expands to a comma only when variadic arguments were actually passed — It solves the dangling-comma problem: LOG("boot") with zero extra args would otherwise leave "..., __LINE__," hanging. __VA_OPT__ emits its content only if __VA_ARGS__ is non-empty.
Macro or function? A field guide
| macro | function | |
|---|---|---|
| type checking | none — text | full |
| argument evaluated | 0, 1, or many times | exactly once |
| works on any type | yes ("generic" for free) | one signature |
can use sizeof/types/# | yes | no |
| debugger & error messages | see the expansion, ouch | clean |
| address can be taken | no | yes |
Modern advice: prefer real functions (the compiler inlines them beautifully — see the inline lesson), and reserve macros for what functions cannot do: stringifying, token pasting, using __FILE__/__LINE__ at the call site, and type-generic tricks.
Try it
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
You now command substitution; next comes the preprocessor's other superpower — making whole regions of code appear or vanish with #if.