Operator precedence versus order of evaluation
Asked Answered
G

4

5

A friend asked me to explain the difference between operator precedence and order of evaluation in simple terms. This is how I explained it to them :-

Let's take an example -

int x;
int a = 2;
int b = 5;
int c = 6;
int d = 4;

x = a * b / (c + d);

Here, the final value of x will become 1. This is because first, the values of c and d will be added together (6+4), then the values of a and b will be multiplied together (2*5), and finally, the division will take place (10/10), resulting in the final value becoming 1, which is then assigned to x.

All of this is specified by operator precedence. In this example, the parentheses force the addition to take place before the multiplication and the division, even though addition has a lower precedence. Also, the multiplication is executed before the division, because multiplication and division have the same precedence, and both of them have the associativity of left-to-right.

Now comes the important part, i.e. the order of evaluation of this expression.

On one system, the order of evaluation may be like this -

/* Step 1 */   x = a * b / (c + d);
/* Step 2 */   x = a * 5 / (c + d);
/* Step 3 */   x = a * 5 / (c + 4);
/* Step 4 */   x = a * 5 / (6 + 4);
/* Step 5 */   x = a * 5 / 10;
/* Step 6 */   x = 2 * 5 / 10;
/* Step 7 */   x = 10 / 10;
/* Step 8 */   x = 1;

Note that in any step, it is always ensured that the operator precedence is maintained, i.e. even though b was replaced by 5 in Step 2, the multiplication did not take place until Step 7. So, even though the order of evaluation is different for different systems, the operator precedence is always maintained.

On another system, the order of evaluation may be like this -

/* Step 1 */   x = a * b / (c + d);
/* Step 2 */   x = a * b / (6 + d);
/* Step 3 */   x = a * b / (6 + 4);
/* Step 4 */   x = a * b / 10;
/* Step 5 */   x = 2 * b / 10;
/* Step 6 */   x = 2 * 5 / 10;
/* Step 7 */   x = 10 / 10;
/* Step 8 */   x = 1;

Again, the operator precedence is maintained.

In the above example, the entire behaviour is well-defined. One reason for this is that all of the variables are different. In technical terms, the behaviour in this example is well-defined because there are no unsequenced modifications to any variable. So, on any system, x will always get assigned the value 1 finally.

Now, let's change the above example to this :-

int x;
int y = 1;

x = ++y * y-- / (y + y++);

Here, the final value that gets assigned to x varies between systems, making the behaviour undefined.

On one system, the order of evaluation may be like this -

/* Step 1 */   x = ++y * y-- / (y + y++);   // (y has value 1)
/* Step 2 */   x = ++y * y-- / (1 + y++);   // (y still has value 1)
/* Step 3 */   x = ++y * 1 / (1 + y++);     // (y now has value 0)
/* Step 4 */   x = 1 * 1 / (1 + y++);       // (y now has value 1)
/* Step 5 */   x = 1 * 1 / (1 + 1);         // (y now has value 2)
/* Step 6 */   x = 1 * 1 / 2;
/* Step 7 */   x = 1 / 2;
/* Step 8 */   x = 0;

Again, the operator precedence is maintained.

On another system, the order of evaluation may be like this -

/* Step 1 */   x = ++y * y-- / (y + y++);   // (y has value 1)
/* Step 2 */   x = ++y * y-- / (y + 1);     // (y now has value 2)
/* Step 3 */   x = ++y * 2 / (y + 1);       // (y now has value 1)
/* Step 4 */   x = ++y * 2 / (1 + 1);       // (y still has value 1)
/* Step 5 */   x = ++y * 2 / 2;             // (y still has value 1)
/* Step 6 */   x = 2 * 2 / 2:               // (y now has value 2)
/* Step 7 */   x = 4 / 2;
/* Step 8 */   x = 2;

Again, the operator precedence is maintained.

How can I improve this explanation?

Golter answered 25/5, 2021 at 13:40 Comment(14)
What's wrong with the explanation?Calaboose
I just wanted to know whether this is absolutely correct. I don't want to mislead them.Golter
Well, as far as I can see it's correct. But, hmmm, it DOES invoke UB, so you have no guarantee that the evaluation takes place at all.Calaboose
But I asked a related question that you might find useful https://mcmap.net/q/1921341/-is-it-undefined-behavior-to-use-functions-with-side-effects-in-an-unspecified-order/6699433Calaboose
Alright! I'll add this as well.Golter
It contains a lot of misconceptions. First of all the Why can't we mix increment operators like i++ with other operators? bug. And then you might want to have a look at What is the difference between operator precedence and order of evaluation?.Marc
@Marc Could you please tell me about the aforementioned misconceptions? It'll really help me get a clear idea about this whole thing.Golter
I don't think the proposed dupe is very good so I vote to reopen. Any way - explaining something by using an example that has UB should IMO be avoided.Genteel
I would use function calls instead of variables. IMO that will make it more clear that an evaluation is involved.Genteel
the final value that gets assigned to x varies between systems, making the behaviour undefined. Hm, it's the other way round. The behavior is undefined, so it can vary between systems, the compiler can do what it wants. the order of evaluation may be When it's undefined, it's undefined, anything may be. Compiler may calculate what it wants, how it wants. You can write "then the compiler will format your hard drive" or "compiler will spawn demons" and it will be as much correct as any evaluation order in case of undefined behavior.Centripetal
@KushagrJaiswal Just read the links I posted. In particular, the order of evaluation of sub expressions generally doesn't care about what operator that happens to sit between the operands. Except for all the special cases like && etc of course.Marc
@Genteel Agreed. But, they couldn't understand this concept from en.cppreference.com/w/c/language/eval_order. So, I had to find a different example.Golter
If you're trying to differentiate between precedence and order of operations, then among the key things to do is avoid using temporal and / or ordering terms such as "first" and "before" to describe the effects of precedence. Precedence is about what, not when.Edgewise
Your statement "the parentheses force the addition to take place before the multiplication and the division" is wrong -- they only force the addition to be before the division. It would be perfectly acceptable for the multiplication to occur before the addition, as the multiplication operands are independent of the addition.Foreordain
G
10

I would prefer an explanation that uses function calls. A function call makes it very obvious that "something needs to be evaluated before applying the operator".

Basic example:

int x = a() + b() * c();

must be calculated as

temp = result_of_b_func_call * result_of_c_func_call
x = result_of_a_func_call + temp

due to multiplication having higher precedence than addition.

However, the evaluation order of the 3 function calls is unspecified, i.e. the functions can be called in any order. Like

a(), b(), c()
or
a(), c(), b()
or
b(), a(), c()
or
b(), c(), a()
or
c(), a(), b()
or
c(), b(), a()

Another basic example would be to explain operator associativity - like:

int x = a() + b() + c();

must be calculated as

temp = result_of_a_func_call + result_of_b_func_call
x = temp + result_of_c_func_call

due to left-to-right associativity of addition. But again the order of the 3 function calls are unknown.

If function calls is not an option, I would prefer something like

x = a * b + c / d

Here it's pretty obvious that there are two sub-expressions, i.e. a * b and c / d. Due to operator precedence both of these sub-expressions must be evaluated before the addition but the order of evaluation is unspecified, i.e. we can't tell whether the multiplication or the division is done first.

So it can be

temp1 = a * b
temp2 = c / d
x = temp1 + temp2

or it can be

temp2 = c / d
temp1 = a * b
x = temp1 + temp2

All we know is that the addition must be last.

Genteel answered 25/5, 2021 at 14:27 Comment(0)
P
4
6.5 Expressions
...
3     The grouping of operators and operands is indicated by the syntax.85) Except as specified later, side effects and value computations of subexpressions are unsequenced.86)
85) The syntax specifies the precedence of operators in the evaluation of an expression, which is the same as the order of the major subclauses of this subclause, highest precedence first. Thus, for example, the expressions allowed as the operands of the binary + operator (6.5.6) are those expressions defined in 6.5.1 through 6.5.6. The exceptions are cast expressions (6.5.4) as operands of unary operators (6.5.3), and an operand contained between any of the following pairs of operators: grouping parentheses () (6.5.1), subscripting brackets [] (6.5.2.1), function-call parentheses () (6.5.2.2), and the conditional operator ? : (6.5.15). Within each major subclause, the operators have the same precedence. Left- or right-associativity is indicated in each subclause by the syntax for the expressions discussed therein.

86) In an expression that is evaluated more than once during the execution of a program, unsequenced and indeterminately sequenced evaluations of its subexpressions need not be performed consistently in different evaluations.
C 2011 Online Draft

Precedence and associativity only control how expressions are parsed and which operators are grouped with which operands. They do not control the order in which subexpressions are evaluated.

Given your example

x = a * b / (c + d);

precedence and associativity cause the expression to be parsed as

(x) = ((a * b) / (c + d))

The multiplicative operators * and / have the same precedence and are left-associative, so a * b / (c + d) is parsed as (a * b) / (c + d) (as opposed to a * (b / (c + d))).

So what this tells us is that the result of a * b is divided by the result of c + d, but this does not mean that a * b must be evaluated before c + d or vice versa.

Each of a, b, c, and d may be evaluated in any order (including simultaneously if the architecture supports it). Similarly each of a * b and c + d may be evaluated in any order, and if the same expression is evaluated multiple times in the program, that order doesn't have to be consistent. Obviously both a and b have to be evaluated before a * b can be evaluated, and both c and d have to be evaluated before c + d can be evaluated, but that's the only ordering you can be certain about.

There are operators that force left-to-right evaluation - ||, &&, ?:, and the comma operator, but in general order of evaluation is a free-for-all.

Pomelo answered 25/5, 2021 at 15:42 Comment(0)
I
2

It's not necessarily true to say that the "the parentheses force the addition to take place before the multiplication and the division". You can see this in a disassembly of the code (gcc 10.2.0):

x = a * b / (c + d);
   1004010b6:   8b 45 fc                mov    -0x4(%rbp),%eax
   1004010b9:   0f af 45 f8             imul   -0x8(%rbp),%eax
   1004010bd:   8b 4d f4                mov    -0xc(%rbp),%ecx
   1004010c0:   8b 55 f0                mov    -0x10(%rbp),%edx
   1004010c3:   01 d1                   add    %edx,%ecx
   1004010c5:   99                      cltd
   1004010c6:   f7 f9                   idiv   %ecx

The multiplication was performed first, followed by the addition, then the division.

Irrelative answered 25/5, 2021 at 14:36 Comment(0)
I
2

Nope, you say

Here, the final value of x will become 1. This is because first, the values of c and d will be added together (6+4), then the values of a and b will be multiplied together (2*5), and finally, the division will take place (10/10), resulting in the final value becoming 1, which is then assigned to x.

the evaluation order establishes that 6 + 4 will be evaluated before the division is done... but not that the compiler cannot first arrange to evaluate first c * d (because the multiplication operators are left associative, and this means --also-- that the multiplication will be made before the division). You don't even know (except if you look at the assembler output) which order of subexpression evaluation will the compiler select. As stated, the full parenthesized expression would be:

(x = ((a * b) / (c + d)));

so, the compiler will decide to start first with a * b or c + d indistinctly. Then it will do the other operation, then it will do the division, and finally the assignment. But beware, because the assignment requires the address of x and not its value (it's an lvalue), so the address of x can be calculated at any point, but before the assignment is made. Finally, the (unused) value of the assignment is thrown.

a possible order could be:

  • calculate a * b
  • calculate address of x
  • calculate c + d
  • calculate the division (a*b)/(c+d)
  • store the result at position &x.

a different one:

  • calculate c + d
  • calculate a * b
  • calculate the division (a*b)/(c+d)
  • calculate address of x
  • store the result at position &x.

but you could also calculate the address of x in the first step.

Idocrase answered 27/5, 2021 at 20:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.