Skip to content
VLSI Mentor

C++ Part I · Module 4 · Expressions and Operators

Precedence, Associativity and Evaluation Order

Two chapters in this module have ended by telling you to add parentheses, and both were pointing at the same table. Bitwise operators bind more loosely than comparisons; logical not binds more tightly than almost everything; assignment groups right to left while most operators group left to right. Those are not separate facts to memorise — they are one mechanism, and understanding it removes a whole category of bug that otherwise reappears every few months. The chapter also separates that mechanism from something it is constantly confused with. Precedence decides which operands belong to which operator, and C++ specifies it completely. Evaluation order decides when each part of an expression actually runs, and C++ deliberately leaves much of it open — with one notable exception you already rely on, and one set of rules that genuinely changed in the version this curriculum teaches. Merging those two questions is the most common thing people get wrong about C++ expressions, so the chapter keeps them apart from the first paragraph.

Intermediate16 min readC++Part IFundamentalsExpressions

Part I · Module 4 · Chapter 4.9 · Precedence, Associativity and Evaluation Order

1. Two Different Questions

Given an expression like a + b * c, there are two questions you can ask, and they are not the same question.

Which operands belong to which operator? Is this a + (b * c) or (a + b) * c? That is precedence, and C++ answers it completely. There is a fixed table, it is the same in every conforming implementation, and the answer never varies.

When does each part actually run? Does b get evaluated before c? If both are function calls with side effects, which happens first? That is evaluation order, and C++ answers it only partly. For many expressions there is no answer at all — the standard permits either order.

These sound like one topic and behave like two:

PrecedenceEvaluation order
DecidesWhich operands bind to which operatorWhen each sub-expression runs
Specified?CompletelyOften not at all
Varies by compiler?NoYes, legitimately
Fix when unclearParenthesesSeparate statements

Parentheses do not fix evaluation order. Writing (f()) + (g()) changes nothing about which runs first. That single sentence is the most useful thing in this chapter, and §5 is why.

2. Precedence

Precedence is the rule for how tightly an operator holds its operands. * binds more tightly than +, so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    a + b * c       // means  a + (b * c)

This matches ordinary arithmetic and causes no trouble. Trouble comes from the levels nobody memorised.

3. The Six Levels That Actually Cause Bugs

A full precedence table has around seventeen levels. Memorising it is the wrong goal — you would still parenthesise, because your readers have not memorised it either.

What is worth knowing is the handful where intuition is wrong. Here they are, tightest first:

LevelOperatorsThe trap
1++ -- (prefix), !, ~, unary -! binds tighter than everything below — Chapter 4.5 §6
2* / %—
3+ -—
4<< >>Looser than arithmetic: a << b + c is a << (b + c)
5< <= > >= then == !=—
6& then ^ then |Looser than comparison — Chapter 4.8 §5
7&& then ||—
8= and the compound formsLoosest, and groups right to left — Chapter 4.6 §4

Three entries have already bitten in this module:

! is tighter than &. Chapter 4.5 §6 measured it: !flags & MASK means (!flags) & MASK, which gave false where !(flags & MASK) gave true.

& is looser than !=. Chapter 4.8 §5 measured it: reg & 0x0010u != 0 means reg & (0x0010u != 0), testing bit 0 instead of bit 4. Verified: that form gives false while (reg & 0x0010u) != 0 gives true.

<< is looser than +. value << 1 + 2 shifts by 3, not by 1 and then add 2. This one also bites in output statements, which is why std::cout << a + b works but std::cout << a == b does not do what it looks like.

The common thread: the bitwise and shift operators sit in the wrong places. C set their precedence before && and || existed, and C++ inherited it. Everyone agrees it is a wart, and nobody can change it without breaking decades of code.

The rule that makes all of this a non-issue: parenthesise any sub-expression involving a bitwise or shift operator, and any use of ! that is not immediately obvious.

That is not timidity. It is knowing which levels are counter-intuitive and refusing to make readers reconstruct them.

4. Associativity

Associativity settles ties: when two operators of the same precedence meet, which groups first?

Most operators group left to right:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    a - b - c       // means  (a - b) - c
    a / b / c       // means  (a / b) / c

That matters — (10 - 4) - 3 is 3, and 10 - (4 - 3) is 9.

A few group right to left, and you have met two:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    a = b = c       // means  a = (b = c)      — Chapter 4.6 §4
    !!value         // means  !(!value)

Chapter 4.6 §4 explained why assignment has to group this way: left-to-right would make chained assignment meaningless.

5. Evaluation Order Is a Different Thing Entirely

Now the second half, and the part that surprises people.

Precedence tells you that f() + g() means "add the result of f() to the result of g()". It tells you nothing about whether f runs before g.

For most operators, C++ does not specify the order. The compiler may evaluate either operand first, and may choose differently in different places in the same program.

Here it is measured, with two calls that each have a side effect:

Azvya Education Pvt. Ltd.VLSI Mentor
argorder.cpp — complete program. The order is unspecified.
#include <iostream>

static int next_id(int& counter) { return ++counter; }
static void show(int a, int b) { std::cout << "a=" << a << " b=" << b << '\n'; }

int main()
{
    int counter = 0;
    show(next_id(counter), next_id(counter));   // order unspecified
    return 0;
}

On the machine used for this chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic argorder.cpp -o argorder
$                                            ← no warning
$ ./argorder
a=1 b=2
$ c++ -std=c++17 -O2 argorder.cpp -o argorder && ./argorder
a=1 b=2

Consistent across optimisation levels, and that proves nothing. The standard permits a=2 b=1, and a different compiler, version or target may produce it. Chapter 1.3 §5 measured the same program and drew the same conclusion:

Reproducible is not guaranteed. Observation tells you what your toolchain did; only the standard tells you what every toolchain must do.

Note also that no warning appeared. The compiler is not obliged to tell you that you have written something order-dependent, and generally it cannot — the dependency is only a problem if the sub-expressions interact, which it would have to prove.

6. The Exceptions You Can Rely On

A short list of operators that do sequence their operands. These are guarantees, not habits:

  • && and || — the left operand is evaluated first, and the right may not be evaluated at all. Chapter 4.5 §3 is built entirely on this.
  • The comma operator, a, b — the left is evaluated and its result discarded, then the right. Verified: i++, i++; starting from 5 gives 7, with no warning.
  • The conditional operator, cond ? a : b — the condition first, then exactly one of the branches.
  • Simple assignment in C++17 — in E1 = E2, the right-hand side is sequenced before the left. §7.

Everything else: assume nothing.

7. What C++17 Changed

A note on a rule that moved, because you may have learned the older one.

C++17 tightened the sequencing rules for several operators. The one that matters most here is simple assignment, and the effect is visible:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int i = 5;
    i = i++;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++14 -Wall -Wextra -Wpedantic seq.cpp -o seq
seq.cpp:5:10: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]

$ c++ -std=c++17 -Wall -Wextra -Wpedantic seq.cpp -o seq
$                                           ← no diagnostic

Under C++14 this modified i twice with no sequencing between the modifications — undefined behaviour. Under C++17 the right-hand side is sequenced before the left, which gives the expression a defined meaning.

The change was partial. This is still undefined in C++17:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int r = i++ + i++;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
seq2.cpp:5:14: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]

+ was not given a sequencing guarantee, so modifying i twice across it remains undefined.

Chapter 4.7 §6 drew the practical conclusion and it is worth repeating here, because this chapter is where the mechanism lives:

Being defined is not the same as being readable. A reader cannot tell i = i++ from i++ + i++ by looking, and the difference depends on which -std the build used. Modify an object once per expression and the question never arises.

8. What to Actually Do

Four habits that make this chapter unnecessary in practice.

Parenthesise bitwise and shift sub-expressions. Always, when they appear alongside anything else. The precedence is counter-intuitive and your reader will not reconstruct it.

Parenthesise anything you had to think about. If you paused to work out the grouping, the next reader will pause too — and may not pause long enough.

One side effect per expression. The order questions in §5 only matter when two parts of an expression interact. If at most one part changes something, there is nothing to get wrong.

Split rather than nest. When an expression needs an order, give it one with statements:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const int first  = next_id(counter);     // this one first
    const int second = next_id(counter);     // then this one
    show(first, second);

Three lines, no ambiguity, and the intent is in the code rather than in a rule the reader has to know.

9. Mistakes Worth Making Once

Merging precedence and evaluation order. Precedence is fully specified; evaluation order often is not. Parentheses fix the first and do nothing for the second.

Assuming arguments evaluate left to right. Measured: consistent on one machine, unspecified in the language.

Reading consistent output as a guarantee. Chapter 1.3 §5's lesson, and this chapter's program demonstrates it again.

Expecting a warning for order dependence. None appeared. The compiler generally cannot prove the parts interact.

Writing reg & MASK != 0. & is looser than !=. Measured in Chapter 4.8 §5: it tests the wrong bit.

Writing value << n + 1. << is looser than +, so this shifts by n + 1.

Assuming C++17 made all such expressions defined. It pinned down some operators and not others. i++ + i++ still warns.

Relying on i = i++ being defined. It is, in C++17. Nobody reading your code will be sure, and the answer changes with a build flag.

10. What You Should Be Able to Say Now

  1. Precedence decides which operands bind to which operator and is completely specified.
  2. Evaluation order decides when each sub-expression runs and is frequently unspecified.
  3. Parentheses fix precedence and do nothing for evaluation order. Separate statements fix order.
  4. The precedence levels that cause bugs: ! binds very tightly; <</>> are looser than arithmetic; &, ^, | are looser than the comparisons; assignment is loosest.
  5. Measured: reg & 0x0010u != 0 is false while (reg & 0x0010u) != 0 is true, because != binds tighter than &.
  6. The bitwise operators sit where they do for historical reasons, and everyone treats it as a wart.
  7. Most operators group left to right; assignment and the unary prefix operators group right to left.
  8. Measured: show(next_id(c), next_id(c)) gave a=1 b=2 at both -O0 and -O2, with no warning — and the order remains unspecified.
  9. Reproducible is not guaranteed.
  10. Operators that do sequence: &&, ||, the comma operator, the conditional operator, and simple assignment in C++17.
  11. C++17 changed assignment sequencing, so i = i++ warns under -std=c++14 and not under -std=c++17. i++ + i++ is still undefined and still warns.
  12. The order is left open so compilers can schedule freely; the cost falls on expressions whose parts interact.
  13. Write expressions whose parts do not interact, and the whole subject becomes irrelevant.

11. Exercises

Exercise 1 — add the parentheses. Rewrite each to show the grouping the compiler uses: (a) a + b * c (b) reg & MASK != 0 (c) !flags & MASK (d) value << n + 1 (e) a = b = c.

Exercise 2 — which question is it? For each, say whether it is a precedence question or an evaluation-order question: (a) does b * c happen before the +? (b) in f() + g(), does f run first? (c) does & bind tighter than ==? (d) in a = b = c, which assignment happens first?

Exercise 3 — parentheses will not help. A colleague fixes an order-dependent expression by writing (f()) + (g()). Explain why that changes nothing and give the fix that works.

Exercise 4 — read the measurement. The argument-order program printed a=1 b=2 at two optimisation levels. State precisely what that does and does not establish, and name the chapter that made the same point earlier.

Exercise 5 — which are sequenced? From this list, say which guarantee an order between their operands and which do not: &&, +, ||, *, the comma operator, function arguments, ?:.

Exercise 6 — two similar lines. Explain why i = i++ is defined in C++17 while i++ + i++ is not, and say why a reviewer should reject both anyway.

Exercise 7 — rewrite for clarity. Rewrite so that no reader needs to know a precedence rule, and say how many separate facts your version removes the need to know:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (status & 0x0F == mode && !ready | forced) { /* ... */ }

12. Summary

  • Two different questions. Precedence binds operands and is fully specified; evaluation order times them and often is not.
  • Parentheses fix precedence only. Order is fixed by separate statements.
  • The traps: ! very tight; <</>> looser than arithmetic; &, ^, | looser than comparisons; assignment loosest and right-grouping.
  • Measured: reg & 0x0010u != 0 tests the wrong bit.
  • Most operators group left to right; assignment and unary prefix group right to left.
  • Argument order is unspecified — measured consistent on one machine, which guarantees nothing.
  • No warning appears for order dependence; the compiler generally cannot prove it matters.
  • Sequenced exceptions: &&, ||, comma, ?:, and C++17 simple assignment.
  • C++17 changed part of this — i = i++ warns under C++14 and not C++17; i++ + i++ still warns under both.
  • The real fix is design: one side effect per expression, and split when an order is needed.

One subject remains in this module, and it has been running underneath every chapter. Chapter 4.2 said operands are converted before an arithmetic operator runs. Chapter 4.4 showed a comparison where that conversion produced the wrong answer. Chapter 4.6 showed a compound assignment converting a result back and losing data silently. Chapter 4.8 showed a mask inverted at the wrong width. Every one of those was a conversion the programmer did not write and did not see. Chapter 4.10 gathers them into one subject, and gives you the way to say that a conversion is intended — so that the ones you meant look different from the ones that happened to you.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.