Skip to main content

Evaluation Hazards & Hygiene

intermediate17 min readLesson 122 of 148

Double evaluation, missing parens, and the argument that runs twice. Why MAX(a, b) has bitten every C codebase.

The argument that runs twice

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int i = 0;
int m = MAX(i++, 5);      /* expands: ((i++) > (5) ? (i++) : (5)) */

i++ appears twice in the expansion โ€” the ternary evaluates the winner a second time. Undefined behavior, wrong results, and it compiles clean. The fix is not "more parens"; the argument must be evaluated exactly once:

/* GCC/clang statement-expression, NOT ISO C: */
/* ISO C honest alternative: a static inline function */
static inline int max_int(int a, int b) { return a > b ? a : b; }

Inline functions type-check, evaluate arguments once, and debug normally โ€” they are the default answer. Macros stay for cases where the type must stay open (MAX on any numeric type) โ€” and then the contract documented is "arguments must be side-effect free."

Precedence traps outside the macro

#define DOUBLE(x) (x) + (x)      /* wrong at the seam */
int r = 2 * DOUBLE(3);           /* 2 * (3) + (3) == 9, not 12 */

Every macro body must be parenthesized as a whole too: #define DOUBLE(x) ((x) + (x)). The rule is mechanical: wrap every parameter occurrence and the entire expansion.

Hygiene: names that escape

A macro's "local" names are not local โ€” they can collide with the caller's:

#define SWAP(a, b) do { int tmp_ = a; a = b; b = tmp_; } while (0)
/* caller's variable named tmp_? silent corruption */

Convention: underscore-suffixed internal names (tmp_, i_) reduce โ€” never eliminate โ€” collisions. Token pasting with unique names is the next lesson's tool.

Now practice

Macro GymSafe-by-construction macros: ARRAY_LEN, bounded helpers, do-while wrappers.1 challenge ยท ยท ~24 min