Unspecified evaluation order for function arguments in C
0 reputation · 20 Sept 2021, 05:58 UTC
0 reputation · 20 Sept 2021, 05:58 UTC
C leaves the order in which function arguments and operands of most operators are evaluated unspecified, and it leaves sequencing of side effects to sequence points. This documented unspecified behavior means the same source can be legal yet produce different observable results across implementations and optimization levels.
The goal is to characterize the portability boundary for code that must remain consistent across compilers and targets. The constraint is the standard's intentional lack of ordering guarantees, which creates an unresolved decision for developers about whether to rely on observed behavior or rewrite to avoid dependence.
Does the C standard provide any sequencing guarantees for argument evaluation beyond sequence points? Which expressions are guaranteed to have a defined evaluation order in current standards? When does reliance on a particular order become non-portable across conforming implementations?
The C standard provides no sequencing guarantees for the evaluation of function arguments. The order in which arguments are evaluated is unspecified, meaning the compiler is free to evaluate them in any order (left-to-right, right-to-left, or otherwise) without violating the language specification.
\n\nWhile function arguments are unspecified, the C standard does define a strict evaluation order for a small subset of expressions:
\n&& (Logical AND) and || (Logical OR) operators guarantee short-circuiting. The left operand is evaluated first; the right operand is evaluated only if the result is not yet determined.(expr1, expr2), expr1 is guaranteed to be evaluated and its side effects completed before expr2 is evaluated.Reliance on a specific evaluation order becomes non-portable the moment a single function call contains multiple arguments that modify or read the same variable. This is a critical distinction between unspecified behavior and undefined behavior:
\n\n| Scenario | \nClassification | \nResult | \n
|---|---|---|
| Arguments are independent; order varies by compiler. | \nUnspecified | \nLegal, but results may differ across targets. | \n
| One argument modifies a variable that another argument reads. | \nUndefined | \nIllegal; may cause crashes or unpredictable data. | \n
To verify if your code depends on unspecified behavior, you can use a simple diagnostic function to print arguments. Note that the order they appear in the function body is determined by the calling convention (e.g., stack growth), not the evaluation order.
\n\n// Potential portability risk: side effects in arguments\nvoid check(int a, int b) { printf("%d, %d\\n", a, b); }\n\nint main() {\n int x = 0;\n // UNPORTABLE: Order of x++ is unspecified\n check(++x, ++x);\n return 0;\n}\n\n\nTo ensure portability, move expressions with side effects into separate statements before the function call:
\nint a = ++x;\nint b = ++x;\ncheck(a, b); // Guaranteed and portable\nUse comments to ask for clarification. Post a solution as an answer.
33,775 reputation · 20 Sept 2021, 14:31 UTC
One clarification worth adding: since C11 the standard dropped "sequence points" in favor of a sequenced-before relation, and the classification sharpens. Function arguments are not merely evaluated in an unspecified order — they are unsequenced relative to each other. That means f(i++, i) is not "unspecified but defined"; it is outright undefined behavior, because two unsequenced evaluations touch the same scalar object and at least one is a write.
So the portability boundary is binary, not gradual:
Practical check: GCC and Clang flag many cases with -Wsequence-point (on by default with -Wall), and splitting arguments into sequenced statements with temporaries removes the hazard entirely.