The Macro That Counts Twice
A code review finds this metrics header in a telemetry gateway:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define CLAMP(x, lo, hi) ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))
A test contains:
int i = 5;
int r = MAX(i++, 2);
int j = 150;
int c = CLAMP(j++, 0, 100);
The author says the test shows r == 6 and c == 100, so the macros are acceptable. A reviewer replies that the macros are unsafe even where the result happens to look right, and that another call site can invoke undefined behavior. The code must remain ISO C11; GNU statement expressions and typeof are not allowed. Builds use -Wall -Wextra -Wpedantic. A second call site reads MAX(read_adc(), read_adc()), and the ADC read function has a side effect that clears a hardware flag.
1. Expand `MAX(i++, 2)` fully. What are the final values of `i` and `r`, and how many times is `i++` evaluated? Show the evaluation path.
Read the worked answer
The expansion is:
int r = ((i++) > (2) ? (i++) : (2));
With i initially 5, the condition evaluates the first i++: it yields 5 and increments i to 6. The comparison 5 > 2 is true, so the second operand of ?: is evaluated. That second i++ yields 6 and increments i to 7. The result assigned to r is therefore 6, and i ends at 7.
So r == 6 is true, but i is not 6; it is 7. The author's observation that the result looks right is misleading because the macro evaluated the argument twice. If the condition had been false, the second evaluation would not have occurred, so the number of side effects depends on runtime values. That is exactly the kind of hidden behavior that makes function-like macros dangerous with arguments that have side effects.
2. Expand `CLAMP(j++, 0, 100)` for `j == 150`. How many increments occur, and is the behavior undefined? Explain the sequencing.
Read the worked answer
The expansion is:
int c = ((j++) < (0) ? (0) : ((j++) > (100) ? (100) : (j++)));
With j initially 150: the first j++ yields 150 and sets j to 151. 150 < 0 is false. The second j++ yields 151 and sets j to 152. 151 > 100 is true, so the result is 100 and the third j++ is not evaluated. Final j is 152, so two increments occurred, not one.
This is not undefined behavior. C11 has a sequence point between evaluation of the first operand of ?: and evaluation of the selected second or third operand. Each j++ is therefore sequenced relative to the next one. The behavior is unspecified only in the sense that the final value depends on the path taken. If j were 50, all three branches' tests would cause three increments and the function would return 52, not 50. The macro is wrong because it mutates its argument multiple times, even though it does not formally invoke undefined behavior in this call.
3. Give a call to `MAX` that does invoke undefined behavior, explain why, and propose a portable C11 replacement that still works for `int` and avoids double evaluation.
Read the worked answer
A call such as MAX(i++, i++) expands to ((i++) > (i++) ? (i++) : (i++)). The two operands of > are unsequenced relative to each other, and both modify i. C11 6.5/2 says that if a side effect on a scalar object is unsequenced relative to another side effect on the same object, the behavior is undefined. The compiler may produce any result, including one that looks plausible in a debug build.
A portable replacement is a static inline function:
static inline int max_int(int a, int b) {
return a > b ? a : b;
}
Calling max_int(i++, i++) still has a problem: the order of evaluation of function arguments is unspecified, and two unsequenced modifications of i remain undefined. But max_int(i++, 2) evaluates i++ exactly once, because the argument is evaluated before the call. That is the important fix for ordinary call sites. The read_adc() case becomes max_int(read_adc(), read_adc()): the order of the two ADC reads is unspecified, but each is called exactly once, so the hardware flag is cleared twice rather than three times. If the order matters, evaluate into separate variables first. For generic types, use _Generic to select typed inline functions, or write separate functions; do not use typeof or statement expressions in ISO C11.