5.1.28 · D2C Programming

Visual walkthrough — Macros vs inline functions

1,795 words8 min readBack to topic

Step 1 — What a macro literally is: a find-and-replace robot

WHAT. Before your program is turned into machine code, a tiny robot called the preprocessor reads your source as plain text — like words on paper, not like maths. When it sees a #define, it memorises a rule: "wherever you later see this word, cross it out and paste that text in its place."

WHY this tool and not the compiler? The compiler is the smart part that understands numbers, types, and the order of operations. But it never even sees your macro name — the robot has already erased it. That is the entire source of every trap on this page: the substitution happens before anyone who understands maths shows up.

PICTURE. Look at the figure. On the left is what you typed. The robot underlines every occurrence of SQUARE(x) and, on the right, staples the replacement text in — no thinking, no arithmetic, just scissors and glue.

Figure — Macros vs inline functions

Step 2 — The naïve macro: what "no parentheses" really means

WHAT. Take the deliberately broken macro from the parent note:

Here x is the parameter slot: a placeholder the robot will fill. * is literal multiplication text. There are no brackets around the slots — remember that, it is the whole crime.

WHY show the broken one first? Because the danger is invisible until you see the slot get filled with something that is not a single number. A lone 5 in the slot behaves; an expression like 2 + 3 does not.

PICTURE. The figure shows the template x * x with two empty amber slots. Each slot is where the robot will paste — and, crucially, it pastes whatever text you passed, uncooked.

Figure — Macros vs inline functions

Step 3 — Paste the argument in (the moment the bug is born)

WHAT. We write BAD(2 + 3). The robot fills each x slot with the literal text 2 + 3:

Read the result as a flat string of characters: 2 + 3 * 2 + 3. Nobody has multiplied anything yet — this is still just text.

WHY it's a trap. You intended "(the number 5) squared". But the robot never grouped your 2 + 3. It dropped the raw characters straight next to a *, gluing a 3 to the multiplication.

PICTURE. Watch the two amber 2 + 3 blocks slide into the slots. The dashed group you wanted (a box around each 2 + 3) is drawn faintly — and it is missing from the actual pasted text.

Figure — Macros vs inline functions

Step 4 — Now the compiler arrives and obeys precedence

WHAT. Only now does the smart compiler read the finished text 2 + 3 * 2 + 3 and apply the ordinary rules of arithmetic — see Operator Precedence in C. Multiplication binds tighter than addition, so it groups the * first:

  • The 3 * 2 in the middle: those two threes are from different copies of your argument — the tail of the first paste and the head of the second.
  • The outer 2 + and + 3 are leftovers dangling on each end.

WHY not 25? Because the grouping you assumed — one box around each pasted 2+3 — never existed in the text. Precedence sees a lone *, not a squared sum.

PICTURE. The figure highlights 3 * 2 glowing first (evaluated first), then the two orphan terms folding in to give 11, with the wished-for 25 crossed out beside it.

Figure — Macros vs inline functions

Step 5 — The fix: parenthesize every slot and the whole body

WHAT. Rewrite it as the parent note's safe form:

Now SQ(2 + 3) pastes into guarded slots:

  • Inner (x) — the box the robot forgot in Step 3. It forces each pasted 2 + 3 to be added before multiplying.
  • Outer ( ... ) — guards against a different attack: if the whole macro sits inside a bigger expression like 1 / SQ(2), the outer parens stop that from mis-grouping.

WHY two layers? One layer fixes what's inside; the other fixes what's around. You need both because you can't predict where the caller will drop the macro.

PICTURE. Same paste as Step 3, but now solid amber boxes wrap each slot and the whole expression — the addition happens inside the box, the answer is a clean 5, then 5×5.

Figure — Macros vs inline functions

Step 6 — The second trap: counting how many times an argument runs

WHAT. Precedence is fixed, but text-pasting has a deeper wound. Consider SQ(i++) with the correct macro. It pastes into both slots:

Here i++ is not just a value — it is a command with a side effect: "use i, then add 1 to i." Because the text i++ was copied twice, the increment command runs twice.

WHY this is worse than 11. Getting 11 is wrong-but-defined. Running i++ twice between two sequence points is Undefined Behavior in C — the standard makes no promise about the result; different compilers give different numbers, and it can silently change with optimisation.

PICTURE. Two little counters labelled i++ sit in the two slots; the figure shows i starting at 3 and being nudged upward twice, with a red "⚠ evaluated 2×" tag on the side effect.

Figure — Macros vs inline functions

Step 7 — Why the inline function evaporates both traps

WHAT. The inline function

is compiled as a real function. When you call square(2 + 3) or square(i++), the argument is evaluated to a single value first, and that one value is handed to the parameter x.

  • square(2 + 3): the compiler computes 2 + 3 = 5 once, then runs 5 * 5 = 25. No precedence trap — there is nothing to re-glue, because a value (5) went in, not the text 2 + 3.
  • square(i++): i++ runs exactly once to produce a value; the side effect happens once. The body then multiplies that stored value by itself. No double increment.

WHY it's safe by construction. A function has a parameter — a labelled box that holds one evaluated value. Inside the body, x names that box, not the caller's original text. And because it's inside the compiler, Function Call Overhead is still removed when it inlines — you pay nothing yet keep the safety.

PICTURE. The argument funnels through a single "evaluate once" gate into the box x; the body reads that box twice, but the input was computed only once. Contrast panel shows the macro's two-gate leak from Step 6.

Figure — Macros vs inline functions

The one-picture summary

WHAT. One frame compresses the whole page: the same call travels down two pipelines. Down the macro road, SQ(2+3) is pasted as text, and only afterwards does precedence bite → the risk of 11 and double side-effects. Down the inline road, 2+3 is evaluated to 5 first, then squared → always 25, always once.

Figure — Macros vs inline functions
Recall Feynman retelling — say it out loud in plain words

Two helpers can square a number for you. The paste helper (macro) doesn't understand maths at all; he just photocopies whatever you wrote into two spots. So if you wrote "2 + 3", he glues "2 + 3 * 2 + 3", and the maths-teacher who checks it later — obeying "times before plus" — gets 11, not 25. Worse, if you wrote "add one to i" (i++), he photocopies that command into both spots, so i gets bumped twice — a result nobody can even predict. The smart helper (inline function) first works out what you meant — "2 + 3 is 5" — writes the single number 5 into one labelled box, and only then squares it: 25, every time, with any side effect happening exactly once. Fix the paste helper by drawing boxes (parentheses) around every slot and around the whole thing — but the smart helper needed no fixing, because he thought before he copied. Both skip the long walk to a faraway kitchen (the Function Call Overhead); only one of them trips on the way.


Connections

  • C Preprocessor — the robot that does the text pasting in Steps 1–3
  • Operator Precedence in C — the rule that turns pasted text into 11 in Step 4
  • Function Call Overhead — the cost both techniques remove
  • Undefined Behavior in C — the double-evaluation danger in Step 6
  • const and constexpr — typed, value-based alternatives to constant macros
  • Compiler Optimization — decides whether the inline body from Step 7 is actually pasted in