Increment Operator Side Effects in C Macros

Passing an increment operator into a function-like macro in C leads to serious side effects primarily caused by multiple evaluations of the argument. Because the C preprocessor relies on simple textual substitution rather than true function call semantics, any argument passed with a side effect—such as x++ or ++x—will be executed every time that parameter appears in the macro definition. This frequently results in unintended duplicate increments, logic errors, and even undefined behavior.

Textual Substitution vs. Function Evaluation

In standard C functions, arguments are evaluated once before the function body executes. By contrast, a preprocessor macro performs pure textual replacement before the code is compiled. The macro does not calculate a value once and store it; instead, it injects the literal expression into the code wherever the parameter name appears.

Consider a common macro definition intended to return the maximum of two values:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

If this macro is called using standard variables, it behaves as expected:

int x = 5;
int y = 10;
int result = MAX(x, y); // Expands to: ((x) > (y) ? (x) : (y))

The Multiple Evaluation Side Effect

The problem arises when an increment operator is passed as an argument:

int i = 5;
int j = 2;
int result = MAX(i++, j);

During preprocessing, the macro expands to:

int result = ((i++) > (j) ? (i++) : (j));

The execution flow of this expansion causes the following steps:

  1. The condition (i++) > (j) is evaluated. Since 5 > 2 is true, i is incremented to 6.
  2. Because the condition was true, the ternary operator evaluates the second branch: (i++).
  3. The value 6 is returned for result, and i is incremented a second time to 7.

Instead of i being incremented once, it is incremented twice. If j had been larger than i, i would have only been incremented once (in the condition). This dynamic means the variable is modified a variable number of times depending on runtime values, making bugs difficult to detect and reproduce.

Risk of Undefined Behavior

Using increment operators in macros can also trigger undefined behavior. Consider the square macro:

#define SQUARE(x) ((x) * (x))

Invoking SQUARE(i++) expands to:

((i++) * (i++))

In C, modifying a scalar object more than once between sequence points (or unsequenced modifications in C11 and later) results in undefined behavior. The compiler is free to evaluate the two operands in any order, apply increments at arbitrary times, or produce unpredictable assembly code. The result cannot be relied upon across different compilers or optimization levels.

How to Avoid Macro Side Effects

To eliminate the risks associated with the increment operator in macros, developers use several alternative techniques:

  • Use inline Functions: In modern C (C99 and later), static inline functions provide the same performance benefits as macros through inlining, but they enforce standard function call semantics where arguments are evaluated exactly once:

    static inline int max(int a, int b) {
        return (a > b) ? a : b;
    }
  • Evaluate Outside the Macro: If a macro must be used, increment the variable on a separate line before invoking the macro:

    i++;
    int result = MAX(i, j);
  • GCC Statement Expressions: In GNU C, macros can be written using statement expressions to assign parameters to temporary, local variables, ensuring each argument is evaluated only once:

    #define MAX(a, b) ({ \
        __typeof__(a) _a = (a); \
        __typeof__(b) _b = (b); \
        _a > _b ? _a : _b; \
    })