Skip to content
VLSI Mentor

C Part I · Module 3 · Expressions and Operators

Evaluation Order and Side Effects

Chapter 3.10 showed how any expression groups — which operands belong to which operator, settled completely and without ambiguity. It then ended on a warning that is the whole subject of this chapter: knowing the shape of an expression tells you nothing about the order in which its pieces are worked out. C deliberately leaves that order open in most expressions, which costs you nothing until an expression starts changing things, at which point it stops being a curiosity and becomes the reason certain respectable-looking lines have no defined meaning at all. This is the most important chapter in the module. It replaces the left-to-right assumption everyone arrives with, names what C actually guarantees, and leaves you with one engineering rule that keeps you clear of the entire problem.

Intermediate14 min readCPart IFundamentalsCorrectness

Part I · Module 3 · Chapter 3.11 · Evaluation Order and Side Effects

1. Three Different Questions

Given an expression, there are three questions you can ask, and almost everyone collapses them into one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  1. Grouping           Which operands belong to which operator?
  2. Evaluation order   Which subexpression is worked out first?
  3. Side-effect timing When does a change to an object actually land?

Chapter 3.10 answered the first completely. It also said, twice, that it was answering only that one. This chapter is about the other two.

The reason they feel like one question is that for expressions which merely read values, they may as well be. (a + b) * (c + d) gives the same answer no matter which half is worked out first. The three questions only come apart once an expression changes something — and Chapters 3.4 and 3.7 gave you two ways to do that.

Four stages explaining the difference between grouping and evaluation order. First, the expression as written. Second, grouping, which precedence and associativity settle completely and identically on every implementation. Third, evaluation order, which asks which operand is worked out first and which C leaves open for most operators. Fourth, the consequence: if the operands have side effects, the result may differ between implementations or be undefined.Grouping is settled. Order is not.01THE EXPRESSION AS WRITTENleft of the operator, right of the operator02GROUPING — SETTLED BY CHAPTER 3.10the same on every implementation, every time03EVALUATION ORDER — A SEPARATE QUESTIONfor most operators C does not fix it at all04IF THE OPERANDS CHANGE THINGSthe order now matters, and nothing promised you one
Figure 1 — the same expression, two different questions. Grouping is fixed by Chapter 3.10's rules and is the same everywhere. The order in which the two operands are worked out is a separate matter, and for most operators C does not fix it at all — a conforming implementation may do either, and need not be consistent.

2. C Does Not Evaluate Left to Right

Here is the belief to abandon:

The expression is written left to right, so C works it out left to right.

It is not true, and it is not a near-miss. For most binary operators, C simply does not specify which operand is evaluated first. An implementation may evaluate the right one first, may evaluate the left one first, and is not required to be consistent between two expressions, two compilations, or two optimisation settings.

This is not an oversight. Leaving the order open is what lets a compiler arrange work in whatever way suits the machine it is targeting — which is Chapter 1.1's point about the standard constraining observable behaviour rather than steps, arriving one more time.

For expressions that only read values, you will never notice. a + b produces the same sum either way.

3. Where It Starts to Matter

Now combine that with side effects. Consider two expressions that both modify objects:

Azvya Education Pvt. Ltd.VLSI Mentor
safe: different objects, one change each
#include <stdio.h>

int main(void)
{
    int a = 0;
    int b = 0;

    int total = (a = 2) + (b = 3);

    printf("total %d, a %d, b %d\n", total, a, b);
    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total 5, a 2, b 3

This is fine, and it is worth understanding why it is fine rather than just noting that it works. The two operands change different objects, and neither reads the object the other one writes. So whichever order the compiler picks, both changes happen and the sum is 5. The order is genuinely unspecified and genuinely does not matter.

Now change one thing — make both operands touch the same object — and the expression stops having a meaning.

4. The Rule That Decides

Here is the rule, which is worth reading slowly because it is the whole chapter:

If an expression changes an object, and that change is unsequenced relative to either another change to the same object or a reading of that same object, the behaviour is undefined.

Three words carry the weight.

Change means a side effect — an assignment, or ++ / --.

Unsequenced means C has not said which happens first. Two things are unsequenced when nothing in the language fixes an order between them.

Undefined means what Chapter 1.3 established: the standard imposes no requirements at all.

So these are not expressions with surprising answers. They are expressions with no answer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
i = i++ + 1;          /* changes i twice, unsequenced     — undefined */
x = y++ + y++;        /* changes y twice, unsequenced     — undefined */
x = i++ + i;          /* changes i and reads it elsewhere — undefined */

This chapter will not show you what any of them prints. Running one produces a number, and the only lesson available from that number is the harmful one — that it means something. Chapters 2.6, 3.7 and 3.9 made the same refusal for the same reason.

Your compiler will often catch the obvious shapes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: multiple unsequenced modifications to 'i' [-Wunsequenced]

Clang reports exactly that for all three lines above. It is a genuinely useful diagnostic — and it is a heuristic, not a guarantee. Expressions assembled across several names and types can slip past it.

5. What C Does Guarantee

The picture is not uniformly bleak. A few constructs do fix an order, and you have already met the most useful of them.

&& and ||. Chapter 3.6 established this and it is worth seeing in its proper context now: the left operand is fully evaluated, side effects and all, before the right operand is considered at all. That is a real sequencing guarantee, and it is why short-circuiting can be relied upon rather than merely observed.

Azvya Education Pvt. Ltd.VLSI Mentor
a guarantee you can build on
#include <stdio.h>

int main(void)
{
    int n = 0;
    int result = ((n = 5) > 0) && (n == 5);

    printf("result %d, n %d\n", result, n);
    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
result 1, n 5

The left operand assigns 5 to n and the right operand reads n. In almost any other expression that combination would be undefined — a change to an object and a reading of it, unsequenced. Here it is well defined, because && guarantees the left side completes first.

The end of a full expression. All side effects in a statement are complete before the next statement begins. That is why splitting a dangerous expression into separate statements fixes it, and it is the basis of §6's rule.

Assignment's own operands. In a = b, the value computations of both sides are sequenced before the store into a. That is what makes x = x + 1 well defined — one change, and the read happens before it.

6. The Rule to Carry

All of the above reduces to one engineering habit, and it is worth more than the theory:

If correctness depends on which of two side-effecting subexpressions happens first, put them in separate statements.

Separate statements are sequenced. There is no ambiguity, no diagnostic to rely on, and no rule to remember.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* instead of packing it into one expression */
i = i++ + 1;

/* say what you mean, in order */
i = i + 1;

The second is clearer, shorter in meaning if not in characters, and defined. There is no cleverness being sacrificed — the compact version was never faster, and it was never correct.

Two supporting habits follow:

One change per object per expression. Chapter 3.7 gave you this rule without its reason; this chapter is the reason.

Do not assume argument order. When a function receives several arguments, the order in which they are worked out is not fixed either. printf("%d %d\n", i++, i++); is undefined for exactly the reason in §4, and it is a popular example precisely because it looks so harmless.

7. What You Should Be Able to Say Now

  1. Grouping, evaluation order and side-effect timing are three different questions. Chapter 3.10 answered the first.
  2. C does not evaluate operands left to right. For most operators it fixes no order at all, and need not be consistent.
  3. That costs nothing for expressions that only read values.
  4. An unsequenced change to an object, alongside another change or a read of that same object, is undefined behaviour.
  5. Undefined means no requirements — not "unpredictable", and not confined to the value of that line.
  6. i = i++ + 1, x = y++ + y++ and x = i++ + i have no answer, so none is shown here.
  7. && and || do guarantee an order: the left operand completes before the right is considered.
  8. Statement boundaries sequence everything, which is why splitting an expression fixes it.
  9. In a = b, both sides are evaluated before the store, which is what makes x = x + 1 fine.
  10. Compilers catch many of these with -Wunsequenced and cannot catch all.
  11. Function argument order is not fixed either.
  12. If order matters, use separate statements.

8. Exercise

Exercise 1 — separate the questions. For (a + b) * (c + d), state what Chapter 3.10's rules fix and what they leave open. Then say under what circumstance the open part would start to matter.

Exercise 2 — classify, do not evaluate. For each, say whether it is well defined or undefined, and give the reason. Do not attempt a value for the undefined ones: (a) total = (a = 1) + (b = 2); (b) n = n++ + 1; (c) x = y++ + z++; (d) result = (p = 1) && (p == 1); (e) count++;

Exercise 3 — explain the exception. Expression (d) above changes p and reads p in the same expression, which §4 says is usually undefined. Explain in two sentences why this particular one is fine.

Exercise 4 — repair it. Rewrite each as correct code that plainly does what the author probably intended: (a) i = i++ + 1; (b) printf("%d %d\n", i++, i++);

Exercise 5 — correct the description. A colleague says: "i = i++ + 1; is undefined, so the value of i afterwards is unpredictable." Using §4's callout, explain what is wrong with the second half of that sentence.

Exercise 6 — reason about a guarantee. You need a check whose first part makes the second part safe to evaluate. Which operator gives you that guarantee, which chapter established it, and what would go wrong if C had not specified it?

9. Summary

  • Grouping is not a timeline. Chapter 3.10 settles which operands belong to which operator and says nothing about when they are worked out.
  • C does not promise left-to-right. For most operators no order is fixed, and consistency is not required.
  • That is harmless until an expression changes something.
  • The rule: an unsequenced change to an object, alongside another change or a read of the same object, is undefined behaviour.
  • Undefined means no requirements — a stronger and worse statement than "unpredictable".
  • && and || do guarantee order, which is what makes short-circuiting dependable rather than merely observable.
  • Statement boundaries sequence everything.
  • Function argument order is not fixed.
  • If order matters, write separate statements. Nothing is lost.

This module has now left the same debt outstanding five times. Integer division produced a whole number, a comparison reversed its answer, ~ inverted more bits than were written, a shift happened in a type nobody declared, and in each case the explanation was deferred to the same place. The last chapter of Module 3 is that place, and it turns out all five were one mechanism.

Standards & specifications

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

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 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.