5.1.28 · D4C Programming

Exercises — Macros vs inline functions

2,611 words12 min readBack to topic

Two rules we will use over and over, so let us name them once:

Every trap below is just one of these two rules biting you.


Level 1 — Recognition

L1.1 — Who runs it?

For each line, say whether the preprocessor or the compiler is responsible.

#define PI 3.14159        // (a)
static inline int sq(int x){ return x*x; }  // (b)
int y = sq(5);            // (c) — who decides to inline?
Recall Solution L1.1
  • (a) Preprocessor. #define is a directive; it runs before real compilation and just remembers "replace the token PI with the text 3.14159."
  • (b) Compiler. inline is a real C keyword parsed and type-checked by the compiler.
  • (c) The compiler (its optimizer) decides whether to actually paste the body here. inline is only a hint.

L1.2 — Text or value?

#define DBL(x) (x)+(x). Someone writes DBL(3). What text does the preprocessor produce (not the number)?

Recall Solution L1.2

Pure text substitution: (3)+(3). The preprocessor never computes 6; it only rewrites text. The compiler later evaluates (3)+(3) = 6.

L1.3 — Which can take an address?

You want a pointer to reuse the "square" behaviour: int (*f)(int) = ???. Can this be a macro SQUARE? Can it be sq?

Recall Solution L1.3

Only the inline function sq has an address, so int (*f)(int) = sq; is legal. A macro is not an object — it vanishes after preprocessing — so &SQUARE is meaningless. This is one power inline functions have that macros never can.


Level 2 — Application

L2.1 — The precedence trap

#define SQ(x) x*x
int r = SQ(2 + 3);

Hand-expand the text, then give the numeric value of r. What value did the author want?

Recall Solution L2.1

Blind paste: SQ(2 + 3)2 + 3*2 + 3. Now apply operator precedence* binds tighter than +: So r = 11. The author wanted . The missing parentheses let the outer + slice the argument apart.

L2.2 — Fix it

Rewrite SQ so SQ(2 + 3) gives 25, then re-expand to prove it.

Recall Solution L2.2

Parenthesize the whole body and every parameter:

#define SQ(x) ((x)*(x))

Expand SQ(2 + 3)((2 + 3)*(2 + 3)). ✅ The inner parens keep 2+3 whole; the outer parens protect the result if the macro sits inside a bigger expression like 1 - SQ(2).

L2.3 — Outer-paren check

With the fixed #define SQ(x) ((x)*(x)), evaluate int r = 100 / SQ(2);. Then evaluate the same with a body that forgot the outer parens, #define BADSQ(x) (x)*(x).

Recall Solution L2.3
  • Good: 100 / ((2)*(2)) = 100 / 4 = 25.
  • Bad (BADSQ): 100 / (2)*(2)/ and * are left-associative and equal precedence, so this is (100 / 2) * 2 = 50 * 2 = 100. The missing outer parens let the surrounding / steal into the macro. Answer: 25 vs 100.

Level 3 — Analysis

L3.1 — Double evaluation, counted

#define SQ(x) ((x)*(x))
int i = 4;
int r = SQ(++i);

Ignoring UB concerns for a moment and assuming left-to-right evaluation, how many times is i incremented, and what are the final i and r?

Recall Solution L3.1

Expand: SQ(++i)((++i)*(++i)). The text ++i appears twice, so i is incremented twice: 4 → 5 → 6, giving i = 6. Under naive left-to-right the product is 5 * 6 = 30. Honest caveat: modifying i twice with no sequence point between is genuinely undefined behavior; a real compiler may print anything. The lesson: a macro duplicates the side effect. (The 30 here is just to expose the double increment, not a guaranteed value.)

L3.2 — Inline version, counted

static inline int sq(int x){ return x*x; }
int i = 4;
int r = sq(++i);

Now how many times is i incremented, and what are the final i and r?

Recall Solution L3.2

By the evaluate-once rule, ++i is computed once into the parameter x before the body runs: i becomes 5, x holds 5. Body: x*x = 25. So i = 5, r = 25. Defined, predictable, type-checked. This is the headline reason to prefer inline functions.

L3.3 — The MAX re-evaluation

#define MAX(a,b) ((a) > (b) ? (a) : (b))
int p = 3, q = 5;
int m = MAX(p++, q++);

Trace the expansion and, assuming left-to-right, give m, p, q.

Recall Solution L3.3

Expand: ((p++) > (q++) ? (p++) : (q++)).

  • Compare: reads p=3, q=5, then post-increments both → after compare p=4, q=6. Condition 3 > 5 is false.
  • So the ?: selects the third branch (q++): reads current q=6 as the result, then increments → q=7. Result: m = 6, p = 4, q = 7. The "winner" q was evaluated twice — once in the compare, once when returned. (Formally UB again; shown to expose the double eval.)

Level 4 — Synthesis

L4.1 — The dangling-if bug

#define SWAP(a,b) int t=(a); (a)=(b); (b)=t;
if (cond) SWAP(x,y); else reset();

Explain why this fails to compile / misbehaves, and rewrite SWAP correctly.

Recall Solution L4.1

Expand after the if:

if (cond) int t=(x); (x)=(y); (y)=t; else reset();

The if body is only the first statement int t=(x);. The remaining two lines run unconditionally, and then a bare else appears with no matching if → compile error. Fix with the do{...}while(0) idiom, which packs many statements into one:

#define SWAP(a,b,T) do { T t=(a); (a)=(b); (b)=t; } while(0)
if (cond) SWAP(x,y,int); else reset();   // one statement, ; consumed

do{...}while(0) is a single statement that runs once, so the if/else pairing is preserved and the trailing ; closes it cleanly.

L4.2 — Choose the right tool

For each need, choose macro or inline function and justify in one line: (a) MIN that works for int, double, and long in the same program; (b) a helper is_even(n) used millions of times in a hot loop that must be debuggable; (c) turning a token into a string literal, e.g. STR(hello)"hello"; (d) a compile-time named constant BUFSIZE = 1024 you can also take a typed pointer to.

Recall Solution L4.2
  • (a) Macro. C has no generics; #define MIN(a,b) ((a)<(b)?(a):(b)) is type-agnostic text. (Parenthesize!)
  • (b) Inline function. No call overhead when inlined, single evaluation, and you can set breakpoints. Debuggability rules out a macro.
  • (c) Macro. Stringizing with #x is a preprocessor-only power: #define STR(x) #x. No function can do this.
  • (d) const / constexpr-style constant. Prefer static const int BUFSIZE = 1024; (see const and constexpr) — it is typed and addressable; a #define constant has no address.

L4.3 — Rewrite a macro as a safe inline

Given #define CUBE(x) ((x)*(x)*(x)), write an equivalent int-typed inline function, and state one behaviour that changes for CUBE(i++).

Recall Solution L4.3
static inline int cube(int x){ return x*x*x; }

With the macro, CUBE(i++) pastes i++ three times → i incremented three times (UB). With cube(i++), i is incremented once, x captures that single value, and x*x*x uses it three times safely. Given i = 2: cube(i++) returns 2*2*2 = 8 and leaves i = 3.


Level 5 — Mastery

L5.1 — Full trace with fixed macro and side effects

#define SQ(x) ((x)*(x))
int a = 2;
int b = SQ(a + 1);
int c = SQ(a++);

Assuming left-to-right evaluation, give b, c, and final a. Mark which line is UB.

Recall Solution L5.1
  • b = SQ(a + 1)((a + 1)*(a + 1)) with a = 2(3)*(3) = 9. No side effect, defined. b = 9.
  • c = SQ(a++)((a++)*(a++)). a++ appears twice → naive result 2 * 3 = 6, a ends at 4. This line is undefined behavior (two modifications of a with no sequence point); c = 6, a = 4 shown only to expose the double increment.

L5.2 — Predict the difference macro vs inline

Using the same inputs as L5.1 but with static inline int sq(int x){return x*x;}:

int a = 2;
int b = sq(a + 1);
int c = sq(a++);

Give b, c, and final a, and state why every line is now well-defined.

Recall Solution L5.2
  • b = sq(a + 1): argument a+1 = 3 computed once → 3*3 = 9. b = 9.
  • c = sq(a++): a++ evaluated once — parameter x gets 2, then a becomes 3. Body x*x = 2*2 = 4. c = 4, a = 3.
  • Every line is defined because the evaluate-once rule gives each side effect a single, sequenced execution before the body runs. Compare with L5.1's UB.

L5.3 — The #define constant vs typed constant

You need SIZE = 10 used to declare int buf[SIZE]; and also passed to printf("%d", SIZE);. (a) Show the macro version and what printf receives. (b) Show the typed version and one advantage it gives.

Recall Solution L5.3
  • (a) #define SIZE 10. int buf[SIZE];int buf[10];, printf("%d", SIZE);printf("%d", 10);. Prints 10. Purely textual, untyped.
  • (b) static const int SIZE = 10;. Same array size (in C99+ this is a VLA-style or use an enum/macro for array bounds in strict C; the typed constant is fully addressable). Advantage: it has a type, an address (&SIZE), respects scope, and shows up in the debugger by name — none of which a #define constant offers.

L5.4 — Design challenge: safe generic MAX

The macro MAX(a,b) double-evaluates. Sketch (in words + code) a technique that keeps macro genericity but evaluates each argument once, using GNU statement expressions.

Recall Solution L5.4

Use a statement expression (a GCC/Clang extension) that stores each argument in a typed temporary using typeof, so each is evaluated once:

#define MAX(a,b) ({ __typeof__(a) _x=(a); __typeof__(b) _y=(b); \
                     _x > _y ? _x : _y; })

_x=(a) and _y=(b) each evaluate their argument exactly once into a local, then the comparison uses the locals. MAX(p++, q++) now increments each of p, q once — the best of both worlds, at the cost of being non-portable (extension, not standard C). For portable code, prefer a typed inline function per type. Mnemonic: capture-once, compare-locals.


Score-yourself figure

Figure — Macros vs inline functions

The bar chart above shows, for the recurring SQ(a++) example with a = 2, how the macro and inline results diverge — the macro double-counts the increment (extra height in red), the inline stays clean (green).


Recall One-line summary of every level

Recognition (who runs it) → Application (parenthesize) → Analysis (count evaluations) → Synthesis (do{}while(0), right tool) → Mastery (typed constants + capture-once generics). Every trap = blind paste or duplicated side effect.

Connections

  • C Preprocessor — the engine doing the blind text paste
  • Operator Precedence in C — the L2 precedence traps
  • Function Call Overhead — the cost both tools remove
  • Undefined Behavior in C — the double-evaluation lines (L3, L5)
  • const and constexpr — typed replacements for #define constants
  • Compiler Optimization — why inline is only a hint