Order of evaluation of array indices (versus the expression) in C

The result here is unspecified.

While the order of operations in an expression, which dictate how subexpressions are grouped, is well defined, the order of evaluation is not specified. In this case it means that either global_var could be read first or the call to update_three could happen first, but there’s no way to know which.

There is not undefined behavior here because a function call introduces a sequence point, as does every statement in the function including the one that modifies global_var.

To clarify, the C standard defines undefined behavior in section 3.4.3 as:

undefined behavior

behavior, upon use of a nonportable or erroneous program construct or of erroneous data,for which this International Standard imposes no requirements

and defines unspecified behavior in section 3.4.4 as:

unspecified behavior

use of an unspecified value, or other behavior where this International Standard provides two or more possibilities and imposes no further requirements on which is chosen in any instance

The standard states that the evaluation order of function arguments is unspecified, which in this case means that either arr[0] gets set to 3 or arr[2] gets set to 3.


Order of Left and Right Operands

To perform the assignment in arr[global_var] = update_three(2), the C implementation must evaluate the operands and, as a side effect, update the stored value of the left operand. C 2018 6.5.16 (which is about assignments) paragraph 3 tells us there is no sequencing in the left and right operands:

The evaluations of the operands are unsequenced.

This means the C implementation is free to compute the lvalue arr[global_var] first (by “computing the lvalue,” we mean figuring out what this expression refers to), then to evaluate update_three(2), and finally to assign the value of the latter to the former; or to evaluate update_three(2) first, then compute the lvalue, then assign the former to the latter; or to evaluate the lvalue and update_three(2) in some intermixed fashion and then assign the right value to the left lvalue.

In all cases, the assignment of the value to the lvalue must come last, because 6.5.16 3 also says:

… The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands…

Sequencing Violation

Some might ponder about undefined behavior due to both using global_var and separately updating it in violation of 6.5 2, which says:

If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined…

It is quite familiar to many C practitioners that the behavior of expressions such as x + x++ is not defined by the C standard because they both use the value of x and separately modify it in the same expression without sequencing. However, in this case, we have a function call, which provides some sequencing. global_var is used in arr[global_var] and is updated in the function call update_three(2).

6.5.2.2 10 tells us there is a sequence point before the function is called:

There is a sequence point after the evaluations of the function designator and the actual arguments but before the actual call…

Inside the function, global_var = val; is a full expression, and so is the 3 in return 3;, per 6.8 4:

A full expression is an expression that is not part of another expression, nor part of a declarator or abstract declarator…

Then there is a sequence point between these two expressions, again per 6.8 4:

… There is a sequence point between the evaluation of a full expression and the evaluation of the next full expression to be evaluated.

Thus, the C implementation may evaluate arr[global_var] first and then do the function call, in which case there is a sequence point between them because there is one before the function call, or it may evaluate global_var = val; in the function call and then arr[global_var], in which case there is a sequence point between them because there is one after the full expression. So the behavior is unspecified—either of those two things may be evaluated first—but it is not undefined.