Show Lecture.OrderOfEvaluation as a slide show.
CS253 Order Of Evaluation
Basic Expressions
Consider the expression a+b
, which parses as:
┌───┐
│ + │
└───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ a │ │ b │
└───┘ └───┘
The a and b nodes are children of the + node.
Basic Expressions
How does a+b*c
parse?
┌───┐ ┌───┐
│ * │ │ + │
└───┘ └───┘
⠌ ⠡ ⠌ ⠡
⠌ ⠡ ⠌ ⠡
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ + │ │ c │ │ a │ │ * │
└───┘ └───┘ └───┘ └───┘
⠌ ⠡ ⠌ ⠡
⠌ ⠡ ⠌ ⠡
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ a │ │ b │ │ b │ │ c │
└───┘ └───┘ └───┘ └───┘
The right-hand side, of course. *
has higher precedence than +
,
so a+b*c
is treated as a+(b*c)
, not as (a+b)*c
.
Basic Expressions
Let’s use parentheses. How does (a+b)*c
parse?
┌───┐ ┌───┐
│ * │ │ + │
└───┘ └───┘
⠌ ⠡ ⠌ ⠡
⠌ ⠡ ⠌ ⠡
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ + │ │ c │ │ a │ │ * │
└───┘ └───┘ └───┘ └───┘
⠌ ⠡ ⠌ ⠡
⠌ ⠡ ⠌ ⠡
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ a │ │ b │ │ b │ │ c │
└───┘ └───┘ └───┘ └───┘
The left-hand side, of course. Parentheses mean “do this first”.
The a+b
must be evaluated before the c
.
No
NO!
Really
┌───┐
│ * │
└───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ + │ │ c │
└───┘ └───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ a │ │ b │
└───┘ └───┘
- Your Dear Aunt Sally can take her
PEDMAS
and put it with her
BODMAS;
neither one applies to programming.
- Parentheses do not mean “first”; they mean “together”.
- Consider
(w+x)*(y+z)
; can’t do both first.
- Parentheses determine the shape of the parse tree,
not the order of evaluation.
- Of course, the shape of the tree determines the order of
evaluation, to some extent. Can’t evaluate
+
until
the arguments have been evaluated.
- In this
(a+b)*c
tree, the +
node feeds to the *
node,
so +
must execute before *
.
- However, we can evaluate
c
before the addition, or after,
or simultaneously.
Really
┌───┐
│ * │
└───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ + │ │ c │
└───┘ └───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ a │ │ b │
└───┘ └───┘
Possible orders of evaluation of (a+b)*c
:
a, b, c, +, *
a, b, +, c, *
a, c, b, +, *
b, a, c, +, *
b, a, +, c, *
b, c, a, +, *
c, a, b, +, *
c, b, a, +, *
Why is this difficult?
┌───┐
│ * │
└───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ + │ │ c │
└───┘ └───┘
⠌ ⠡
⠌ ⠡
┌───┐ ┌───┐
│ a │ │ b │
└───┘ └───┘
- We inherited the concept of precedence from mathematics, which has
no side effects, so our mathematical intuition misleads us.
- In
(a+b)*c
, a
, b
, and c
are simple variables,
so it doesn’t matter in which order they are evaluated.
- However, if they’re expressions with side effects, then it does matter:
- increment/decrement
- assignment
- input/output
- function calls
- could change global variables
- could generate output
Some Operators Are Different
- Some operators have guaranteed left-to-right evaluation:
&& ||
,
(in an expression, not as an argument separator)
?:
- Most do not:
+ - * / %
<< >> & | ^
< > <= >= == !=
= += -= *= /= %= <<= >>= &= |= ^=
A user-defined operation (e.g., operator+
) does not have order
determined, because it’s a function called with two arguments. The
order of evaluation of function arguments is unspecified.
Short-circuiting operators
- The operators
&&
and ||
are special—they come with two
guarantees:
- Their left operand will be evaluated first.
- If that answers the question (false for
&&
, true for ||
),
then the right operand is not evaluated.
- This is not an accident of implementation. This is guaranteed
by the language definition.
- Similarly, the ternary operator
?:
guarantees that only one
of its second or third operands will be evaluated:
int n = time(nullptr) > 40 ? 2024'08'30 : sleep(60*60);
cout << n;
20240830
Pre-/Post-Increment/Decrement
This example is bad. Don’t do this:
int n = 1;
cout << ++n * ++n << endl; // 🦡
c.cc:2: warning: operation on ‘n’ may be undefined
c.cc:2: warning: operation on ‘n’ may be undefined
9
Why not?
n
is being modified twice in this expression,
and the order of those modifications can’t be controlled.
Another bad example
Don’t do this, either:
short nums[] = {1, 2, 3, 4};
short *p = nums;
cout << *p++ << ' ';
cout << *p++ << '\n';
p = nums;
cout << *p++ << ' ' << *p++; // 🦡
c.cc:8: warning: operation on ‘p’ may be undefined
1 2
1 2
Why not?
cout << *p++ << ' ' << *p++;
is an expression,
and p
is being modified twice in that expression.
The order of the various parts is unspecified. <<
is just
another operator, like +
, =
, or /=
.
A non-increment example
At this point, students think “Ah—increment is unpredictable.
I’ll avoid it.” That’s not the only potential problem.
int inc() { // Return increasing numbers: 1, 2, …
static int n = 0;
n = n + 1;
return n;
}
int main() {
int a[] = {inc(), inc(), 33, 44, 55}; // 🦡 1,2,33,44,55 or 2,1,33,44,55?
a[inc()] = inc(); // 🦡 a[3] = 4 or a[4] = 3?
for (int n : a)
cout << n << ' ';
}
1 2 33 44 3
Note the lack of any sort of warning.
Why not?
Why not? Because, in all cases above, the variable is being modified
twice in the same expression.
<<
is an operator, just like +
is.
cout << a << b
is one expression.
- same as
(cout << a) << b
- same as
operator<<(operator<<(cout,a), b)
- C++ does not determine the order of execution of the
various parts of the expression, except for a few special cases:
&&
||
?:
,
(the binary comma operator, not function arguments)
Another Example
int a() { cout << 'A'; return 1; }
int b() { cout << 'B'; return 2; }
int main() {
cout << a()+b() << '\n'; // 🦡 Might display AB3 or BA3.
cout << a() << b() << '\n'; // 🦡 Might display A1B2, AB12, BA12
return 0;
}
AB3
A1B2
Experiments don’t matter
You cannot determine the “correct” result by experimentation.
- The compiler might do it in one order today, and in a different order tomorrow.
- The next version of the compiler might do it differently.
- It might depend on what other variables have been declared.
- A different compiler might do things in a different order.
It’s like when your grandmother asks you what kind of music you kids
like these days. You certainly don’t represent everybody in your age
group. Besides, your own tastes may change.
How to do it right
- Synchronization points, such as the semicolon at the end of the line,
guarantee that everything before the synchronization point will be done
by then.
- Break expressions into statements, using explicit temporaries,
to control the order of evaluation:
int a() { cout << 'A'; return 1; }
int b() { cout << 'B'; return 2; }
int main() {
int c = a(); // Will display A
c += b(); // Will display B
cout << a(); // Will display A1
cout << b(); // Will display B2
return 0;
}
ABA1B2