MAX That Skips Half Your Array2026-07-09
This function walks an array once and returns the largest element. Given {10, 25, 15, 40, 20, 35}, we expect 40. Instead, we get 35 — and the sanitizer screams about an out-of-bounds read. What's going on?
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int max_price(const int *prices, int n) {
int max = prices[0];
int i = 1;
while (i < n) {
max = MAX(max, prices[i++]);
}
return max;
}
int main(void) {
int prices[] = {10, 25, 15, 40, 20, 35};
printf("Max: %d\n", max_price(prices, 6)); // want 40, get 35 (and UB)
return 0;
}
MAX is a function-like macro, and the preprocessor doesn't know that prices[i++] has a side effect. The call expands textually to:
max = ((max) > (prices[i++]) ? (max) : (prices[i++]));
That's two occurrences of i++. Whenever the comparison chooses the second branch, i is incremented twice and a different element is what actually gets assigned to max.
Trace with {10, 25, 15, 40, 20, 35}:
max=10, i=1 → compare against prices[1]=25, i=2. 10 > 25 false, evaluate again: read prices[2]=15, i=3. max=15. The 25 vanished.max=15, i=3 → compare against prices[3]=40, i=4. False, evaluate again: read prices[4]=20, i=5. max=20. The 40 vanished too.max=20, i=5 → compare against prices[5]=35, i=6. False, evaluate again: read prices[6] — one past the end. Undefined behavior. Assuming the read returns garbage ≤ 35, max=35.The macro doesn't just return the wrong answer — it walks off the array. In release builds this can silently corrupt neighbouring stack data or return whatever happened to sit past the buffer. Under ASan, it's a hard crash.
The general rule: any macro argument that appears more than once in the expansion is a landmine for side-effecting expressions. MAX(i++, j++), MAX(*p++, x), MAX(read_byte(), threshold) — all broken. The ternary variant even has a second, subtler flaw: it returns an lvalue in some compilers and forces both sides through the same conversion, which trips up type promotion.
Use a static inline function. Each argument is evaluated exactly once by the language rules, and the compiler will inline it just as aggressively as the macro:
static inline int max_int(int a, int b) {
return a > b ? a : b;
}
int max_price(const int *prices, int n) {
int max = prices[0];
for (int i = 1; i < n; i++) {
max = max_int(max, prices[i]);
}
return max;
}
If you truly need a type-generic macro, GCC/Clang's statement-expression + typeof idiom caches each argument in a local, evaluating it once:
#define MAX(a, b) __extension__ ({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})
C11's _Generic or C++'s templates give you a portable, side-effect-safe alternative. If your codebase is stuck on classic macros, at least name them in SHOUTY_CASE and forbid non-trivial arguments in code review — because the compiler will never warn you when you feed one in.
