5.1.28 · D3C Programming

Worked examples — Macros vs inline functions

2,799 words13 min readBack to topic

Before we start, one anchor we will lean on constantly:


The scenario matrix

Every macro/inline problem you will meet is one of these cells. Each worked example below is tagged with the cell it covers, and together they cover the whole grid.

# Case class What can go wrong Covered by
A Precedence — arithmetic argument x*x binds wrong without parens Ex 1
B Precedence — surrounding operators outer -, / grab the un-parenthesized body Ex 2
C Side effect / double evaluation i++, p++ evaluated twice Ex 3
D Zero / degenerate input 0, empty, one-argument Ex 4
E Type behaviour (macro vs inline) int-vs-double, truncation, generic-ness Ex 5
F Control-flow embedding if/else + multi-statement macro Ex 6
G Real-world word problem pick the right tool under real constraints Ex 7
H Exam-style twist nested / mixed traps in one line Ex 8

Example 1 — Cell A: precedence inside the argument

Steps

  1. Substitute the literal text 2 + 3 for x. Why this step? The preprocessor copies characters, so SQ(2 + 3) becomes (2 + 3 * 2 + 3).
  2. Now hand the result to the compiler, which applies real Operator Precedence in C: * before +. Why this step? Precedence is a compiler concern, applied after substitution. So 2 + 3*2 + 3 = 2 + 6 + 3.
  3. Add left to right: . Why this step? All operators are now +, associativity is left-to-right, order doesn't change the sum.

Verify: , not . Sanity: the outer parens around the whole body did not save us — the danger was between the two x uses. The real fix is #define SQ(x) ((x)*(x)), giving ((2+3)*(2+3)) = 25.


Example 2 — Cell B: precedence outside the macro

Steps

  1. Substitute 4 for x: the line becomes 10 - 4 / 2. Why this step? Blind paste — HALF(4) disappears, 4 / 2 appears in its place, unbracketed.
  2. Apply precedence: / binds tighter than -, so it's 10 - (4/2). Why this step? Here we got lucky — / outranks -, so this particular case happens to be safe.
  3. Now change the surrounding operator: int r = HALF(4) * 3; becomes 4 / 2 * 3 = (4/2)*3 = 6, but you wanted ... which is also 6. Try int r = 12 / HALF(4);12 / 4 / 2 = (12/4)/2 = 1, but you wanted . Now it breaks. Why this step? An un-parenthesized body is a landmine that only detonates for some surrounding operators. That unpredictability is exactly why the rule is "parenthesize the whole body too": #define HALF(x) ((x)/2).

Verify: with the safe macro, 12 / ((4)/2) = 12 / 2 = 6. ✅ The moral: wrap each parameter AND the entire body.


Example 3 — Cell C: the double-evaluation trap

Steps

  1. Substitute the text i++ for x: r = ((i++)*(i++));. Why this step? x appears twice in the body, so the side-effecting text i++ is pasted twice.
  2. Count the side effects: two i++ operations means i is incremented twice. But when each is read, and in what order, is Undefined Behavior in C — the C standard gives no guaranteed value. Why this step? Parentheses only fix grouping; they cannot un-duplicate a side effect. On a typical compiler you might see r = 3*4 = 12 or 3*3 = 9 — but you must never rely on it.
  3. Now the inline version: static inline int sq(int x){ return x*x; } then sq(i++). Why this step? The argument i++ is evaluated exactly once to produce the parameter value 3, and then i becomes 4. Inside the body x*x = 3*3 = 9, deterministically.

Verify: inline gives r = 9, i = 4, guaranteed. The macro gives undefined results (double increment). This is the single strongest argument for preferring inline — see Function Call Overhead (which inline also removes when the compiler expands it).


Example 4 — Cell D: zero and degenerate inputs

Steps

  1. POS(0)((0) < 0 ? 0 : (0)). Since 0 < 0 is false, the ternary yields 0. Why this step? The boundary value 0 is not less than 0, so it survives unchanged — correct.
  2. POS(-5)((-5) < 0 ? 0 : (-5)). -5 < 0 is true → yields 0. Why this step? Negative input is clamped, as intended.
  3. POS(-3 + 3)(((-3 + 3)) < 0 ? 0 : ((-3 + 3))). Inner value is 0, 0 < 0 false → yields 0. Why this step? Because each x is parenthesized, the sum -3 + 3 is evaluated as a group before comparison. Without those inner parens, -3 + 3 < 0 would still work here, but POS(a & b) type expressions would misbind — the degenerate arithmetic argument confirms our parens are correct.

Verify: POS(0)=0, POS(-5)=0, POS(-3+3)=0. All . ✅ Degenerate/boundary inputs pass because the ternary uses < (strict), so 0 maps to itself.


Example 5 — Cell E: type behaviour, macro vs inline

Steps

  1. CUBE(2.5)((2.5)*(2.5)*(2.5)). Because 2.5 is a double literal, the compiler does floating multiplication. Why this step? The macro has no type of its own — it inherits whatever type the argument's text carries. So .
  2. icube(2.5): the parameter is int x, so 2.5 is converted to int, truncating to 2 before the body runs. Why this step? An inline function is fully type-checked. Passing 2.5 to int x narrows it to 2, then .
  3. Compare: macro = 15.625, inline = 8. Why this step? This is the double-edged sword. The macro is generic (works for double), the inline is type-safe (loudly commits to int). Neither is "wrong" — you choose based on need. See const and constexpr for typed constant alternatives.

Verify: (macro, kept as double); int truncation of 2.5 is 2, (inline). ✅


Example 6 — Cell F: control-flow embedding

Steps

  1. Substitute: the if body becomes x = 0; y = 0;. So the full text is:
    if (flag)
        x = 0; y = 0;      // <-- two statements!
    else
        z = 1;
    Why this step? An if without braces takes only the next single statement — that's x = 0;. The y = 0; is now a separate statement that always runs, and it sits between the if and the else.
  2. The compiler sees else with no matching ifsyntax error (error: 'else' without a previous 'if'). Why this step? Pasting two statements where one was expected shattered the if/else pairing.
  3. Fix with the do{...}while(0) idiom:
    #define RESET(a,b) do { (a) = 0; (b) = 0; } while(0)
    Now RESET(x,y); expands to do{ (x)=0; (y)=0; } while(0); — one syntactic statement, and the trailing ; is consumed cleanly. Why this step? do{...}while(0) bundles many statements into exactly one statement that also swallows a following semicolon, so it slots into if/else like a normal call would.

Verify: with the idiom, the if(flag) RESET(x,y); else z=1; parses correctly: flag true runs both assignments, false runs z=1. ✅


Example 7 — Cell G: real-world word problem

Steps

  1. Substitute: ((next()) > (threshold) ? (next()) : (threshold)). Why this step? a = next() appears twice. So next() is called once in the comparison and, if it wins, again when "returned".
  2. Case split. If next() <= threshold: next() runs once (comparison), and the : branch returns threshold. If next() > threshold: next() runs twice (comparison and the ? branch) — and each call advances the stream, so it returns a different, later value than the one that won the comparison. Why this step? The bug is intermittent because it only bites when next() > threshold. That's why it "sometimes works." Classic double-evaluation, exactly Cell C but hidden inside a real call.
  3. Fix — inline function, which evaluates each argument once:
    static inline int imax(int a, int b){ return a > b ? a : b; }
    int v = next();
    total += imax(v, threshold);   // next() called exactly once
    Why this step? The inline version computes next() once into a, guaranteeing the compared value and the returned value are the same. With -O2 it inlines to the same fast code — zero call overhead and correct.

Verify (numeric): suppose the stream yields 10, 99 on successive calls and threshold = 5. Macro: comparison call reads 10 (10 > 5 true), return call reads 99 → contributes 99 (wrong, and eats an extra stream value). Inline: reads 10 once, 10 > 5 → contributes 10 (correct). ✅


Example 8 — Cell H: exam-style twist (nested traps)

Steps

  1. SQ(a - b) → substitute text a - b for each x: a - b * a - b. Why this step? Blind paste, no inner parens → 4 - 2*4 - 2.
  2. Apply precedence (* before -): 4 - (2*4) - 2 = 4 - 8 - 2 = -6. Why this step? The middle * steals b and a, wrecking the subtraction grouping.
  3. Now SQ(a - b) * 2a - b * a - b * 2 = 4 - 2*4 - 2*2 = 4 - 8 - 4 = -8. Why this step? The trailing * 2 binds only to the last pasted b, not the whole result — a fourth surprise. The intended is nowhere close.
  4. Correct macro #define SQ(x) ((x)*(x)): SQ(a-b)((4-2)*(4-2)) = 4; SQ(a-b)*2((4-2)*(4-2))*2 = 8. Why this step? Full parenthesization (each param + whole body) makes both cases match the intended algebra.

Verify: buggy: SQ(a-b) = -6, SQ(a-b)*2 = -8. Fixed: 4 and 8. ✅ Every trap traced to text substitution + precedence.



Recall checkpoint

Recall Answers

Q1 ::: The danger is between the two xs (2+3 * 2+3); only per-parameter parens ((x)*(x)) fix it, not just wrapping the body. Q2 ::: Becomes 12/4/2 = 1 (left-to-right) instead of 12/(4/2)=6; fix with ((x)/2). Q3 ::: Macro pastes i++ twice → two side effects, order unspecified → UB; inline evaluates the argument once before the body. Q4 ::: Returns 0; 0 < 0 is false so 0 maps to itself — strict < keeps the boundary correct. Q5 ::: Macro is typeless text → 2.5 stays double15.625; inline int x truncates 2.528. Q6 ::: do { ... } while(0).


Connections

  • Parent: Macros vs inline functions
  • C Preprocessor — the engine doing blind text substitution
  • Operator Precedence in C — why every example turns on precedence
  • Function Call Overhead — the cost inline removes while staying safe
  • Undefined Behavior in C — the double-evaluation cases
  • const and constexpr — typed alternatives to constant macros
  • Compiler Optimization — when inline is actually honored (Ex 7)

Concept Map

fix

fix

fix

Macro call text

Preprocessor pastes text

Precedence trap

Double evaluation trap

Multi statement trap

Parenthesize param and body

Use inline eval once

do while zero idiom

Inline function

Type checked eval once