C's Macro Double-Evaluation Trap: The MAX That Skips Half Your Array

2026-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 35and 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;
}

The Bug

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}:

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.

The Fix

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.

Key Takeaway: A function-like macro evaluates each argument as many times as it textually appears in the expansion — pass any expression with a side effect and you'll skip values, iterate off the end, or corrupt state in ways the preprocessor happily hides from you.

All newsletters