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:
- The condition
(i++) > (j)is evaluated. Since5 > 2is true,iis incremented to6. - Because the condition was true, the ternary operator evaluates the
second branch:
(i++). - The value
6is returned forresult, andiis incremented a second time to7.
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
inlineFunctions: In modern C (C99 and later),static inlinefunctions 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; \ })