C / PREPROCESSOR
Function-like macros and double-evaluation hazards
Write function-like macros correctly and spot when one evaluates an argument twice, then fix the resulting side-effect bugs at the definition or call site.
What you will learn
- Hand-expand a function-like macro to count how many times each argument's text appears
- Tell apart wrong-but-defined double evaluation from outright undefined behavior
- Hoist side-effecting arguments into a local before passing them to a macro
- Read the real expansion with gcc -E instead of guessing at it
Understanding Function-like macros and double-evaluation hazards
A function-like macro is defined by putting a parameter list immediately after the macro name: #define MAX(a, b) ((a) > (b) ? (a) : (b)). The ( must touch the name, or you have instead defined an object-like macro whose replacement text merely begins with a parenthesis. When the preprocessor sees MAX(x, y + 1) it splits the argument list at top-level commas, keeps each argument as a raw token sequence, and copies those tokens into every place the matching parameter appears in the replacement list. Nothing is evaluated, nothing is typed, and nothing is stored in a temporary; the result is just more source text handed to the compiler.
That copying is where the hazard lives. The parameter a appears twice in MAX, so MAX(i++, j) becomes ((i++) > (j) ? (i++) : (j)): two increments, and the value handed back is the one produced by the second increment. Because the expansion contains ?:, how many times an argument runs can also depend on the data, so the same macro may evaluate its argument once on one line and twice on the next. Cost multiplies identically: a parameter mentioned three times can mean three calls to an expensive function or three reads of a hardware register.
Two different kinds of breakage hide under the phrase double evaluation, and sequence points separate them. In MAX(i++, j) the ?: fully sequences the condition before either branch, so the program is well-defined and simply computes the wrong answer. With #define SQ(x) ((x) * (x)), the call SQ(i++) yields (i++) * (i++), where * imposes no ordering between the two modifications of i: that is undefined behavior, not a wrong number. In neither case can the compiler rescue you by evaluating the argument once, because it must honor every side effect the expansion literally contains.
The defensive habits follow directly from the mechanism: prefer replacement lists that mention each parameter exactly once, and at call sites give any expression with a side effect a name first so the macro only ever sees a plain variable.
<stdio.h>
MAX(a, b)
static int calls = 0;
static int reading(void)
{
calls++;
return 30 - 20 * calls; /* 10 on call 1, -10 on call 2 */
}
int main(void)
{
int i = 5, j = 3;
/* expands to: ((i++) > (j) ? (i++) : (j)) */
int m = MAX(i++, j);
printf("m = %d, i = %d\n", m, i);
/* expands to: ((reading()) > (5) ? (reading()) : (5)) */
int n = MAX(reading(), 5);
printf("n = %d, reading() ran %d times\n", n, calls);
return 0;
}A function-like macro substitutes its argument's tokens at every occurrence of the parameter, so an argument's side effects and cost repeat once per occurrence instead of once per call.
Worked examples
A character classifier that eats two characters
Shows how one ++ written by the caller becomes two increments, making a digit test report the wrong answer.
<stdio.h>
IS_DIGIT(c)
int main(void)
{
static const char s[] = "7a";
const char *p = s;
int ok = IS_DIGIT(*p++);
printf("ok = %d, p advanced by %d\n", ok, (int)(p - s));
return 0;
}Example explained
Line 1IS_DIGIT(*p++) expands to ((*p++) >= '0' && (*p++) <= '9'), so the single ++ is written twice.
Line 2&& is a sequence point, so this is well-defined, but the two comparisons inspect two different characters.
Line 3The left test reads '7' and passes; the right test reads 'a' and fails, so a digit is classified as a non-digit.
Line 4p ends up two positions further along, which desynchronizes any scanning loop built on this macro.
Hoisting the argument into a local
A clamp macro that names its value three times calls a sensor three times, until the call site evaluates it once.
<stdio.h>
CLAMP(v, lo, hi)
static int calls = 0;
static int sensor(void)
{
static const int data[] = { 120, 50, 7 };
return data[calls++];
}
int main(void)
{
int bad = CLAMP(sensor(), 0, 100);
printf("direct clamp: %d (sensor calls: %d)\n", bad, calls);
calls = 0;
int raw = sensor();
int good = CLAMP(raw, 0, 100);
printf("hoisted clamp: %d (sensor calls: %d)\n", good, calls);
return 0;
}Example explained
Line 1CLAMP mentions v three times, so CLAMP(sensor(), 0, 100) can call sensor up to three times.
Line 2The first call returns 120 (not below 0), the second returns 50 (not above 100), so the final branch calls again and yields 7.
Line 3Storing the reading in raw first gives the macro a plain variable, whose repeated evaluation is harmless.
Line 4The clamped result is now 100 as intended, from exactly one sensor read.
Important notes
Not every double evaluation is undefined behavior: &&, ||, ?: and the comma operator sequence their operands, so those expansions are merely wrong, while *, +, < and the argument list of a call do not, making two modifications of the same object across them undefined.
The GCC/Clang idiom ({ typeof(a) _a = (a); typeof(b) _b = (b); _a > _b ? _a : _b; }) does evaluate each argument exactly once, but statement expressions are not standard C and the temporary names can collide with an identifier the caller passes in.
Common mistakes
Writing MAX(i++, j) and expecting one increment: the expansion holds two i++, so i advances twice whenever the first argument wins and the returned value is one too large.
Assuming the optimizer will collapse the repeated argument: it cannot, because the repeated expression may have side effects and may return a different value each time, as reading() does above.
Putting a space before the parameter list, as in #define SQ (x) ((x)*(x)): that defines an object-like macro, so SQ(3) expands to (x) ((x)*(x))(3) and the compiler reports an undeclared identifier x.
Try it yourself
Change, predict, then run
Define ABS(x) as ((x) < 0 ? -(x) : (x)), then print ABS(n--) and n for int n = -3; and write out the expansion to account for the value you see. Then fix the call by hoisting n-- out of it, and confirm both the result and the final n change.
Open the C workspaceCheck your understanding
Given #define DOUBLE_IT(x) ((x) + (x)) and int a = 3; int b = DOUBLE_IT(a++); what can you say about the program?
- b is 6 and a is 4, because the argument is evaluated once and its value reused
- b is 7 and a is 5, and this is well-defined because + evaluates its left operand first
- The behavior is undefined, because a is modified twice with no sequence point separating the two modifications
- It does not compile, because a macro argument may not contain the ++ operator
Show answer
The expansion is ((a++) + (a++)). The + operator neither orders its operands nor places a sequence point between them, so two modifications of a in one expression is undefined behavior rather than a merely wrong number. Option 2 in the list is the tempting one because it assumes a left-to-right rule that C guarantees for && and ?: but not for +; the first option describes how a real function call would behave, but a macro pastes tokens instead of passing a value.