Proven C Book←↑→

52 Expressions and operators

What to know first

Chapter 21, Expressions · what becomes a value, and precedence
Chapter 35, Assignment and side effects · evaluation order and sequence points
Chapter 39, Arrays · subscripting and pointer arithmetic
Chapter 47, Structures · member access

Looking back

Chapter 21 said “do not memorise the table, use parentheses”, and since then operators have appeared piecemeal wherever they were needed — shifts in chapter 29, comparisons in chapter 31, assignment in chapter 35, pointer arithmetic in chapter 39. So what is left to learn here?

A. The same material through a different lens. Until now the question was “how much of this operator do I need right here?” From here on we read each operator as a contract: what it accepts (constraints on the operands), what it hands back (result type and value category), and where the contract ends (the grey zones).

That lens earns its keep in practice. Reading someone else’s code and getting stuck; needing to know why the compiler optimised something the way it did; chasing a bug that only appears in one build — all of them come down to the contract of an operator.

The need for this chapter, and its context

Part IX is the part of “what you already learned, again and to the end”. Its first chapter is a complete survey of operators because across the preceding fifty-one chapters operators arrived a few at a time, as needed. Gather them once here and appendix A can be left as a pure lookup table.

By the end of this chapter

Operators learned piecemeal, gathered in one place. We start with how expressions and operators in C differ from those of mathematics and how the standard defines expression, operator, operand and side effect, then what an expression carries (value, type, value category, side effects), then the full precedence and associativity table, then operator-by-operator contracts, evaluation order and sequence points, and finally the grey zones gathered. After this chapter, appendix A is a lookup sheet and nothing more.

The questions this chapter answers

  1. Does an expression without side effects do nothing if you write it? A statement like x + 1;?
  2. Does “lvalue” simply mean “on the left”?
  3. Then when does the i++ in for (i = 0; i < n; i++) happen?
  4. What, then, should be used in C?
  5. Why go as far as UB? Would “it is computed in one of the two orders”, an unspecified rule, not be enough?

52.1 Expressions in mathematics, expressions in C#

“Expression” and “operator” are words we first learned in mathematics. C borrowed them, but the meaning shifted quite a lot. Make the difference clear first, and the rest of this chapter reads as its consequences.

In mathematics an expression is numbers, variables and operation symbols put together by the rules, and it denotes one value. 2 + 3 × 4 denotes 14. An operator is a function that takes operands and gives one result — addition is a function from two reals to one real. And “evaluating” an expression changes nothing in the world. Evaluate x + 1 a hundred times and x is still x; whenever and in whatever order you evaluate it, the value is the same. That is why, in mathematics, x = x + 1 is a false equation.

In C an expression has the same shape, but four things differ.

WhatMathematicsC
what an expression doesdenotes a valuecomputes a value, designates an object or function, changes something, or several of these together
does evaluating change the worldnoit can — x = x + 1 is not an equation but “add one to x”. This is a side effect
range of valuesnumbers have no endevery value has a type, with a finite range. INT_MAX + 1 is outside the contract (chapter 29), and in floating point (a + b) + c and a + (b + c) can differ (chapter 54)
time of evaluationa relation outside timethe machine works in time, step by step, and that order is not fully fixed — “Evaluation order and sequence points”, later in this chapter

Table 52.1 — Expressions in mathematics and in C

★ Where the second and fourth rows meet is the trickiest spot in C expressions. If an expression changes things and the moment of change is not fully fixed, what does code that changes the same object twice in one expression mean? This chapter ends with the standard’s answer to that question.

52.2 The rigorous definition — expression, operator, operand#

The standard defines an expression like this.

“ An expression is a sequence of operators and operands that specifies computation of a value, or that designates an object or a function, or that generates side effects, or that performs a combination thereof. The value computations of the operands of an operator are sequenced before the value computation of the result of the operator.1 ”

The branches joined by “or” are the first row of the table above. With an example each:

BranchExampleNote
specifies computation of a valuea + b, n * 2, x > 0the branch most like mathematics
designates an objectx, *p, a[i], s.namepoints at a place, not a value — an lvalue (chapter 36)
designates a functionprintf, maina function designator: “that function” itself, before it is called
generates side effectsx = 1, i++, puts("hi")what happens besides the computation — next section
a combinationi++ (value computation + side effect), *p = 0 (designation + side effect)most real-world expressions are here

Table 52.2 — The four things an expression does

Note the definition’s second sentence as well. In (a + b) * c, the product’s value is settled only after the values of a + b and c — obvious, but it is the only order the standard guarantees between an operator and its operands. It does not say whether a + b or c is computed first.

Operator and operand are defined where the standard classifies tokens.

“ A punctuator is a symbol that has independent syntactic and semantic significance. Depending on context, it may specify an operation to be performed (which in turn may yield a value or a function designator, produce a side effect, or some combination thereof) in which case it is known as an operator (other forms of operator also exist in some contexts). An operand is an entity on which an operator acts.2 ”

Three practical facts follow.

Expressions build up recursively. The smallest expressions — what the standard calls primary expressions: identifiers, constants, string literals and parenthesised expressions — are the leaves, and operators take expressions as operands to make bigger ones. a + b * c is an addition whose operand is the expression b * c. So an expression is tree-shaped, and what fixes the tree’s shape is precedence and associativity (in the standard’s words, “the grouping of operators and operands is indicated by the syntax”, §6.5.1p3). The outermost expression that is part of no other — all of x = a + b in the statement x = a + b; — is a full expression (§6.8.1p4). That word comes back with sequence points.

52.3 Side effects — what happens besides the value#

The word that decisively separates mathematical expressions from C expressions is side effect. The standard’s definition:

“ A volatile access to an object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression in general includes both value computations and initiation of side effects.3 ”

The list has four entries. In the shapes this book has already met:

The standard’s wordsShapes met in this bookNote
modifying an objectx = 1, x += 2, i++, --nassignment and increment/decrement are the operators that exist for their side effects (chapter 35)
modifying a filefputs, fprintf, fwritescreen output too is writing to the standard output stream (chapter 23)
a volatile accessreading a hardware registereven a read is a side effect — a device can react to having been read (chapter 114)
calling a function that does any of thoseprintf("hi"), rand(), strtok(...)if the body does one of the above, the call itself is a side effect

Table 52.3 — Four kinds of side effect

Of C’s operators, only three cause side effects by themselves — assignment (= and compound assignment), increment/decrement (++, --) and the function call. There is one exception that is not an operator. As the table’s third row says, reading the value of a volatile object is itself a side effect, so an expression that merely reads *reg through volatile int *reg carries a side effect too. The rest (+, *, <, &&, ?: …) only compute values, like the operators of mathematics. But if there is a side effect inside their operands, the whole expression carries it.

examples-en/expressions/side_effects.c

#include <stdio.h>

static int calls;

static bool hit(bool v)          /* changes calls: a side effect */
{
    calls++;
    return v;
}

int main(void)
{
    puts("[1] an expression has a value, and may also do something");
    int x = 0;
    int v = (x = 5) + 1;        /* the assignment has value 5, and x changes */
    printf("v = %d, x = %d\n", v, x);

    int i = 3;
    int post = i++;             /* value 3, effect: i becomes 4 */
    int pre  = ++i;             /* value 5, effect: i becomes 5 */
    printf("post = %d, pre = %d, i = %d\n", post, pre, i);

    puts("\n[2] && skips its right side --- and the side effects in it");
    calls = 0;
    bool r = hit(false) && hit(true);
    printf("result %d, hit() called %d time(s)\n", r, calls);
    calls = 0;
    r = hit(true) || hit(false);
    printf("result %d, hit() called %d time(s)\n", r, calls);

    puts("\n[3] the comma operator: left, then right; value of the right");
    int c = (calls = 0, calls + 10);
    printf("c = %d\n", c);

    puts("\n[4] one change per object between sequence points");
    int k = 0;
    k++, k++;                   /* comma operator: sequence point */
    k++;                        /* each full expression ends one */
    k++;
    printf("k = %d\n", k);
    return 0;
}

Output

[1] an expression has a value, and may also do something
v = 6, x = 5
post = 3, pre = 5, i = 5

[2] && skips its right side --- and the side effects in it
result 0, hit() called 1 time(s)
result 1, hit() called 1 time(s)

[3] the comma operator: left, then right; value of the right
c = 10

[4] one change per object between sequence points
k = 4

The first group separates “value” from “effect”. In (x = 5) + 1 the assignment’s value is 5, so v became 6, and separately the effect of setting x to 5 happened. The value of i++ is the 3 from before the change; the value of ++i is the 5 from after. The second group shows that when && or || does not evaluate its right side, the side effects inside it (here a function call that increments calls) do not happen. The comma operator in the third group evaluates the left and throws it away, giving the right’s value.

Look at the definition’s last words, “initiation of side effects”. Evaluation only starts a side effect; when it finishes is decided by other rules. The value 5 of x = 5 may be settled now, while the moment 5 is actually written into x’s memory can be somewhere later. That gap is the starting point of “Evaluation order and sequence points”, later in this chapter.

Q. Does an expression without side effects do nothing if you write it? A statement like x + 1;?

A. Nothing. And the compiler knows it and skips the computation. The standard allows this — an actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (§5.1.2.4p4). gcc and clang give such statements a -Wunused-value warning (“statement with no effect”).

The same paragraph holds up the optimisation of chapter 14. A computation without side effects has the same observable result whenever it is done, however often, or not at all, so the compiler moves, merges and deletes it at will. Put the other way, side effects are what the compiler cannot casually touch — which is why rules about when side effects must happen are needed.

52.4 The four things an expression carries#

Every expression in C carries four things at once. Keeping them apart is most of what this chapter teaches.

What it carriesWhat that means
ValueThe result of the computation. 2 + 3 has the value 5
TypeWhich container the value sits in — fixed at compile time (chapter 24)
Value categoryWhether it is an lvalue (it designates a place). x is; x + 1 is not
Side effectsWhether it changes an object or the outside world (chapter 35)

Table 52.4 — What an expression carries

Value category sounds like jargon, but you have been using it all along. What may appear on the left of an assignment is an lvalue (chapter 35), and what you may apply & to is an lvalue (chapter 36). An array name is an lvalue that nonetheless cannot be assigned to — a special case (chapter 39).

Q. Does “lvalue” simply mean “on the left”?

A. Historically yes (left value). Today it is more accurate to read it as an expression that designates a place — appearing on the left of an assignment is one consequence of that property. *p is an lvalue even on the right-hand side, and a const int c is an lvalue that is not a modifiable lvalue, so it cannot be assigned to.

That is why the standard says “modifiable lvalue” when it means the stricter thing. You will see that phrase in the operand column for assignment and for increment below.

The notations for writing a constant — bases, prefixes, suffixes, escapes, string literals — are gathered in chapter 21, “Every way of writing a constant”, and the rule that settles an integer constant’s type is in chapter 28. This chapter deals with the operators that join those leaves.

52.5 Precedence and associativity#

The table below lists the operator groups from the highest precedence down: the higher the row, the more strongly its operators bind. Associativity decides which side groups first when operators of the same strength stand side by side — a - b - c is (a - b) - c because it is left-associative, and a = b = 0 is a = (b = 0) because assignment is right-associative.

groupoperatorsassoc.why it associates that way
postfix() [] . -> ++(post) --(post), compound literalL→Ra.b.c only makes sense burrowing from the left
unary++ -- + - ! ~ (type) * & sizeof alignofR→Lthe one nearest the operand binds first: - -x, *&x
multiplicative* / %L→Rthe convention of arithmetic
additive+ -L→Rsubtraction only makes sense left-associative
shift<< >>L→Ra << 1 << 2 pushes in turn
relational< <= > >=L→Rwhich is why x < y < z differs from mathematics
equality== !=L→R
bitwise AND&L→R
bitwise XOR^L→R
bitwise OR|L→R
logical AND&&L→Rshort-circuiting only works from the left
logical OR||L→Rthe same reason
conditional?:R→Lso a ? b : c ? d : e reads as a ladder
assignment= += -= *= /= %= &= ^= |= <<= >>=R→Lso that a = b = 0 makes both zero
comma,L→Rthe left is done first and discarded

Table 52.5 — Associativity, by operator group

A common misconception. “Higher precedence means computed first”

Precedence is a rule for grouping, not an order in time. In f() * g() + h() the multiplication does bind more strongly than the addition, yet nothing is wrong if h() is called before f(). The order of the three calls is unspecified.

You met this distinction in chapter 35 — precedence decides “what goes with what”, and sequence points decide “what finishes before what”. They are different questions, and a later section deals with the second.

52.6 Places where precedence trips people up#

what was writtenhow it really groupsif that was the intent
a & b == ca & (b == c)(a & b) == c
a << 1 + 2a << (1 + 2)(a << 1) + 2
*p++*(p++)(*p)++
*p.x*(p.x)(*p).x or p->x
(int)x + y((int)x) + y(int)(x + y)
a = b = 0a = (b = 0)(as it is — right-associative)
x < y < z(x < y) < zx < y && y < z
!x & y(!x) & y!(x & y)
sizeof a + 1(sizeof a) + 1sizeof(a + 1)
a ? b : c = d(a ? b : c) = d (usually an error)a ? b : (c = d)

Table 52.6 — Where precedence groups it differently than intended

A table is only a table. Running the same thing makes it stick far faster — the demo below prints the parenthesised and unparenthesised forms side by side.

examples-en/expressions/precedence.c

// Precedence, associativity and short circuiting, seen with the eyes.
// Instead of memorising a table, print the parenthesised and unparenthesised forms side by side.
#include <stdio.h>

static int calls = 0;

static int loud(int value)          // leaves a trace every time it is called
{
    calls += 1;
    return value;
}

int main(void)
{
    int x = 6;

    puts("1. precedence --- what binds tighter");
    printf("  2 + 3 * 4      = %d   (same as 2 + (3 * 4))\n", 2 + 3 * 4);
    printf("  (2 + 3) * 4    = %d\n", (2 + 3) * 4);
    printf("  1 << 2 + 3     = %d   (same as 1 << (2 + 3): + binds tighter than <<)\n",
           1 << (2 + 3));

    puts("2. the classic trap --- == binds tighter than &");
    printf("  x & 1 == 0     is read as x & (1 == 0) = %d\n", x & (1 == 0));
    printf("  what was meant: (x & 1) == 0         = %d\n", (x & 1) == 0);

    puts("3. associativity --- which side groups first");
    printf("  10 - 4 - 3     = %d   (left to right: (10 - 4) - 3)\n", 10 - 4 - 3);
    int a, b;
    a = b = 5;                      // assignment groups from the right
    printf("  a = b = 5      gives a=%d b=%d   (right to left)\n", a, b);

    puts("4. comparison does not chain the way mathematics does");
    printf("  1 < 2 < 3      = %d   ((1 < 2) is 1, and 1 < 3 is true)\n", (1 < 2) < 3);
    printf("  3 > 2 > 1      = %d   ((3 > 2) is 1, and 1 > 1 is false)\n", (3 > 2) > 1);

    puts("5. short circuit --- the right side may never run");
    calls = 0;
    if (0 && loud(1))
        puts("  not reached");
    printf("  after 0 && loud(1): loud() was called %d time(s)\n", calls);
    calls = 0;
    if (1 || loud(1))
        (void)0;
    printf("  after 1 || loud(1): loud() was called %d time(s)\n", calls);
    return 0;
}

Output

1. precedence --- what binds tighter
  2 + 3 * 4      = 14   (same as 2 + (3 * 4))
  (2 + 3) * 4    = 20
  1 << 2 + 3     = 32   (same as 1 << (2 + 3): + binds tighter than <<)
2. the classic trap --- == binds tighter than &
  x & 1 == 0     is read as x & (1 == 0) = 0
  what was meant: (x & 1) == 0         = 1
3. associativity --- which side groups first
  10 - 4 - 3     = 3   (left to right: (10 - 4) - 3)
  a = b = 5      gives a=5 b=5   (right to left)
4. comparison does not chain the way mathematics does
  1 < 2 < 3      = 1   ((1 < 2) is 1, and 1 < 3 is true)
  3 > 2 > 1      = 0   ((3 > 2) is 1, and 1 > 1 is false)
5. short circuit --- the right side may never run
  after 0 && loud(1): loud() was called 0 time(s)
  after 1 || loud(1): loud() was called 0 time(s)

Look closely at the fourth group. 1 < 2 < 3 is true while 3 > 2 > 1 is false. Read with the habits of mathematics both should be true, but C groups two at a time from the left and turns each comparison’s result into a number, 0 or 1, which it then hands to the next comparison. The fifth group is the short circuit of && and || — when the left side settles the answer, the right side is not evaluated at all, so anything that “must happen” placed on the right quietly disappears (chapter 33).

The first row of the table before the demo, a & b == c, is counted among C’s famous design scars — that the bitwise operators bind more weakly than the comparisons is a trace of early C, before it had && and ||. So parentheses are effectively mandatory around a bit test. The standard itself notes in a footnote that a<b<c does not read as it does in mathematics.

52.7 Prefix and postfix — the same job, a different value#

++ and -- can go before or after the operand. Knowing exactly how the prefix and postfix forms differ is a good part of the power to read C expressions, so they are gathered here.

52.7.1 The contract the standard sets#

prefix ++x (§6.5.4.1)postfix x++ (§6.5.3.5)the same?
Operanda modifiable lvalue of real or pointer typethe samethe same
What it does to the objectadds 1adds 1the same
The value of the expressionthe value after the changethe value before the changedifferent
Is the result an lvaluenonothe same (C++ differs — below)
How it is defined++E is equivalent to (E += 1)defined separatelydifferent

Table 52.7 — Prefix and postfix increment, compared

The only difference is the value the expression yields. What happens to the object is identical. So in a place where the value is not used — the third slot of a for, a statement that is just i++; — the two mean exactly the same thing.

A common misconception. “It says real type, so it cannot be used on integers”

What the standard calls a real type is not “floating point”. By the classification in §6.2.5 it is integer types and real floating types together, and the only thing left out is complex. So it applies to int, char, bool, double and pointers, and by the standard not to double _Complex.

Measured, GCC lets z++ (complex) through and only says “ISO C does not support ++ and -- on complex types” when -Wpedantic is on — a place it accepts as an extension (chapter 13′s grey area).

52.7.2 When the value is settled, and when memory changes#

This is the heart of the section. In x++ the event of settling the value and the event of changing memory are two different events, and the standard fixes only their order.

WhatThe standard’s sentence
Postfix (§6.5.3.5p2)the value computation of the result is sequenced before the side effect of updating the stored value of the operand
Prefix (§6.5.4.1p2 → §6.5.17.1p3)++E is (E += 1), and in an assignment the side effect of updating the left operand is sequenced after the value computations of both operands
Every expression (§6.5.1p1)the value computations of the operands are sequenced before the value computation of the result

Table 52.8 — The standard’s sentences that order value computation and store in increment

How to read that matters. The standard nails down the relative order, not the moment.

A common misconception. “A postfix increment happens at the end of the statement (at the semicolon)”

A very widespread belief. Nothing in the standard says it. What is settled is only the order — “the result’s value first, the store after” — and when the store actually happens is any time before the next sequence point: possibly just before the first instruction of the next statement, possibly in the middle of the same expression.

The belief is dangerous because it invites the next thought: “so if I use it twice in one expression, the order must be settled”. It is not.

i = i++ + 1;                 /* outside the contract — undefined behaviour */
a[i] = i++;                  /* outside the contract */
printf("%d %d\n", i++, i++); /* outside the contract */

Section 6.5.1p2 nails it: if a side effect on a scalar object is unsequenced relative to another side effect on it or to a value computation using it, the behaviour is undefined. Prefix and postfix are caught alike. GCC reports “operation on ‘i’ may be undefined” through -Wsequence-point (included in -Wall) — though there are many shapes it cannot catch, so do not lean on the warning alone.

Q. Then when does the i++ in for (i = 0; i < n; i++) happen?

A. The third slot is evaluated after the body of each iteration — that is the rule of the for statement, not of the postfix operator (chapter 33). Since nobody uses the result here, switching to prefix does not change one character of the meaning.

The confusing place is where the result is used, as in while (*d++ = *s++);. There, “write what is pointed at now, and move the pointers on” sits in one expression. The two ++ operators touch different objects (d and s), so it is inside the contract. Touch the same object twice and it falls outside — that is the boundary line.

52.7.3 The truth of “prefix is faster”#

In practice. ++ and -- were not created for the PDP-11

The explanation that “++ was made to use the PDP-11′s auto-increment addressing mode” still circulates. Dennis Ritchie, who made C, denied it himself. In “The Development of the C Language” he wrote that people often guess so but it is historically impossible, inasmuch as there was no PDP-11 when B was developed. The PDP-7 did have a few “auto-increment” memory cells, and that probably suggested the operators to Thompson — yet those cells were not used directly in implementing them, and a stronger motivation was probably his observation that the translation of ++x was smaller than that of x=x+1. Generalising them to both prefix and postfix was Thompson’s own doing.

So the “smaller translation” motive was real — but it was a comparison of ++x with x=x+1, not of ++x with x++. Today’s received wisdom is that fact bent once in the retelling.

What about today’s compilers? Measuring with gcc 14 and clang 19 on this machine (x86-64) settles it.

PlaceUnoptimised (-O0)Ordinary build (-O2)
for (…; i++) vs ++i — value unusedthe generated assembly does not differ by one bytethe same
a = (*b)++ vs a = ++(*b) — value usedgcc: 9 instructions vs 11 — the postfix one was the shorter; clang the other way round, the postfix one an instruction longer3 vs 3, identical

Table 52.9 — Code generated for prefix and postfix increment, by optimisation level — x86-64, gcc 14 and clang 19

Two things to read out. First, for C scalars there is no speed difference. Where the value is unused the compiler emits the same code. Second, where the value is used and the code differs, that is not “postfix is slower” but “the two compute different things” — one needs the old value, the other the new.

52.7.4 Two things change in C++#

Platform note. Lvalue-ness, and user-defined types

1. Is the result an lvalue? In C neither prefix nor postfix is. C++ made the prefix one an lvalue. Measured, the two languages part like this.

CodeC (GCC)C++ (G++)
&++xlvalue required as unary '&' operand — erroraccepted
++x = 5lvalue required as left operand of assignment — erroraccepted
&x++errorerror — postfix is a value (prvalue) in C++ too

Table 52.10 — How C and C++ rule on the same code

So code like ++x = 5 compiles in C++ and does not in C. A place to watch in code that crosses between the two languages — and even in C++ it is convention not to write it, because it is hard to read.

2. Postfix on a user-defined type makes a copy. This is the real reason the “prefer prefix” convention took root in the C++ world. A postfix operator has to return the value before the change, so for a class it makes a copy of the old state, keeps it, and returns that.

Attach a counter to the copy constructor and measure: advancing one iterator 1000 times cost the prefix form 0 copies and the postfix form 1000 copies — the same under -O2 (a copy with an observable side effect cannot be optimised away).

C does not have this problem. C’s ++ attaches only to scalars, and a scalar’s “copy” is one register, which is why the difference vanishes in the generated-code table above. Carry the advice “use prefix” straight into C and it becomes a rule without a reason.

Q. What, then, should be used in C?

A. This book’s recommendation.

  • Where the value is unused, make prefix the default. Not for speed but for the signal it gives the reader — “the value of this expression is not used”. It is also the habit that keeps paying when you move to C++. That said, for (i = 0; i < n; i++) has been an idiom since K&R and plenty of codebases keep it. Settle it as a team and hold to it.
  • Where the value is used, write the one you need. Old value: postfix. New value: prefix. Here the computation chooses, not taste.
  • And the one real rule — never touch the same object twice in one expression. Prefix or postfix, keep that and this operator will not hurt you. Chapter 34′s “split statements when the side effects matter” says the same thing.

52.8 Operator by operator#

Now the operators’ contracts, one operator group of the precedence table at a time. Every table has the same columns.

Platform note. What these tables rest on

Checked against the expressions clause (§6.5) of ISO/IEC 9899:2024 (C23). Where an edition changed a rule, that is said in place. If you need to cite a rule, cite the published standard as appendix P explains.

52.9 Postfix operators#

operatoroperandsresultgrey zonein detail
a[i] subscriptone a pointer to a complete object type, the other an integeran lvalue of the pointed-at type. a[i] is *(a+i)UB: access outside the array (including following the one-past-the-end position)chapter 39
f(...) calla function, or a pointer to onethe function’s return type; not an lvalueunspecified: the order in which arguments are evaluated. UB: arguments that disagree with the prototypechapters 22, 25 and 67
s.m membera struct or union value and a member namethe member’s type; an lvalue if the left side is oneUB: reading a union member other than the one last written (the common initial sequence is an exception)chapters 47 and 49
p->m membera pointer to a struct or union, and a member namethe member’s type, an lvalueUB: a null or otherwise invalid pointerchapter 47
x++ post-incrementa modifiable lvalue of real type (integer or real floating; not complex) or pointer typethe value before the change; not an lvalueUB: two unsequenced modifications; signed integer overflowchapter 33
x-- post-decrementthe samethe value before the changethe samechapter 33

Table 52.11 — The contract of the postfix operators

52.10 Unary operators#

operatoroperandsresultgrey zonein detail
++x --xa modifiable lvalue of real or pointer typethe value after the change; not an lvalue — C++ differs++E is (E += 1) — the overflow rules are the samechapter 33
&x address-ofa function designator, the result of [] or unary *, or an lvalue that is not a bit-field and not declared registera pointer to itbreaking the constraint is a compile errorchapter 36
*p indirectiona pointer typean lvalue of the pointed-at typeUB: null, an object whose lifetime has ended, a misaligned address, or one outside its provenancechapters 36, 38 and 45
+xarithmetic typethe promoted valuechapters 21 and 30
-xarithmetic typethe promoted valueUB: signed integer overflow (-INT_MIN)chapter 28
~xinteger typethe bitwise complement after promotionit happens at the promoted width — mind narrow typeschapter 29
!xscalar (arithmetic or pointer)0 or 1, of type intchapter 31
(type)x castbetween scalarsa value of that type; not an lvalueimplementation-defined: pointer↔integer conversion. UB: following a misaligned pointer; converting between function and object pointerschapters 30 and 38
sizeofa complete object type or an expression. Not a function type, an incomplete type, or a bit-fielda size_t valuewith a variable length array the operand is evaluated at run time; otherwise it is not evaluatedchapters 36 and 39
alignofthe name of a complete object type (not an expression)a size_t valuechapter 38

Table 52.12 — The contract of the unary operators

52.11 Arithmetic operators#

operatoroperandsresultgrey zonein detail
* multiplyarithmetic typesthe common type of the usual arithmetic conversionsUB: signed integer overflowchapters 28 and 30
/ dividearithmetic typesthe sameUB: a zero divisor, INT_MIN / -1chapters 29 and 54
% remainderinteger types onlythe sameUB: a zero divisor, INT_MIN % -1chapter 29
+ addboth arithmetic, or a pointer to a complete object type and an integerthe common type, or the pointer typeUB: integer overflow; pointer arithmetic beyond the arraychapters 28 and 39
- subtractboth arithmetic, a pointer and an integer, or two pointers into the same arrayptrdiff_t for pointer differenceUB: subtracting pointers into different arrays; a difference that does not fit ptrdiff_tchapters 28 and 39

Table 52.13 — The contract of the multiplicative and additive operators

Integer division truncates toward zero (settled since C99). So, as long as the quotient is representable, (a/b)*b + a%b == a holds.

52.12 Shift operators#

operatoroperandsresultgrey zonein detail
E1 << E2both of integer typethe type of the promoted left operandUB: E2 negative or at least the width of the promoted E1. UB: E1 signed and negative, or signed and positive with E1 × 2^E2 not representable in the result typechapters 6 and 29
E1 >> E2both of integer typethe sameUB: E2 negative or at least the width. implementation-defined: the result when E1 is signed and negativechapters 6 and 29

Table 52.14 — The contract of the shift operators

A common misconception. “C23 mandated two’s complement, so shifting negatives is defined now”

Two’s complement representation was indeed mandated (C23). The shift clause, however, is unchanged — left-shifting a signed negative value is still UB in C23, and right-shifting a negative value is implementation-defined. Most compilers do an arithmetic shift, but that is a promise of the implementation, not of the standard.

The practical rule is one line: shift on unsigned types. If a signed value must be shifted, move it to an unsigned type, shift, and move it back. And always check that the count is within 0 <= n < width.

52.13 Relational and equality operators#

operatoroperandsresultgrey zonein detail
< <= > >=both real types, or two pointers to compatible object types0 or 1, of type intUB: ordering two pointers that do not belong to the same array (or object)chapters 31 and 38
== !=both arithmetic, compatible pointers, one a void*, one a null pointer constant or nullptr_t, and so on0 or 1, intunspecified: whether a one-past-the-end pointer compares equal to a pointer to the object that followschapters 31 and 37

Table 52.15 — The contract of the relational and equality operators

The two families have different contracts. Equality may be tested between different objects, while ordering only means something within one array. An object that is not an array is treated as an array of length one. For reals, +0.0 and -0.0 compare equal (chapter 54).

52.14 Bitwise and logical operators#

operatoroperandsresultgrey zonein detail
& ^ |both of integer typethe common type of the usual arithmetic conversionsthey reach the sign bit, so use them on unsigned typeschapter 29
&&both scalar0 or 1, intguaranteed: if the left is 0 the right is not evaluated, and there is a sequence point between themchapter 31
||both scalar0 or 1, intguaranteed: if the left is non-zero the right is not evaluatedchapter 31

Table 52.16 — The contract of the bitwise and logical operators

52.15 Conditional, assignment, comma#

operatoroperandsresultgrey zonein detail
c ? a : bc scalar. a and b both arithmetic, or compatible structs or unions, or both void, or compatible pointers, or one a null pointer constantone common type for both branchesguaranteed: a sequence point after the condition; the branch not chosen is not evaluatedchapter 34
=the left must be a modifiable lvaluethe value converted to the left’s type. Not an lvalueUB: assignment between overlapping objects (exact overlap with compatible types is allowed); two modifications within one sequence pointchapter 24
compound op=E1 op= E2as E1 = E1 op E2, except that E1 is evaluated oncethe grey zones of the operation itself (overflow, zero divisor) still applychapter 34
, commaany two expressionsthe type and value of the rightguaranteed: the left is evaluated and discarded, then a sequence point. The comma in an argument list is not this operatorchapter 34

Table 52.17 — The contract of the remaining operators

52.16 Evaluation order and sequence points#

We have seen two things. Evaluating an expression only starts a side effect (the “Side effects” section above), and precedence is a rule about grouping, not about the order in time (chapters 14 and 34). So when can we trust that a side effect has finished? This section is the answer — what this book planted as a seed in chapter 21 and handled piece by piece in chapter 34 and chapter 35 is closed properly here.

52.16.1 Three relations — before, unsequenced, one-or-the-other#

Since C11 the standard writes the order among evaluations as relations.

“ Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread, which induces a partial order among those evaluations. Given any two evaluations A and B, if A is sequenced before B, then the execution of A shall precede the execution of B. … If A is not sequenced before or after B, then A and B are unsequenced. Evaluations A and B are indeterminately sequenced when A is sequenced either before or after B, but it is unspecified which.4 ”

RelationMeaningTypical place
sequenced beforeB starts only after A has finishedleft → right of &&, one statement → the next
indeterminately sequencedone finishes before the other starts, but which comes first is unknownthe two function calls in f() + g()
unsequencedno order at all. The two evaluations may even interleavethe two operands of a + b, the value computations of both sides of x = y

Table 52.18 — The three relations that can hold between two evaluations

The key point is that “unsequenced” is the weakest of the three. It does not mean “one of them comes first and we do not know which” — a machine that does half of A, then B, then the rest of A still conforms. Only function bodies are, exceptionally, not split up. They stand indeterminately sequenced with the caller’s other evaluations (§6.5.3.3p8), so the bodies of two functions do not interleave.

52.16.2 Sequence points — “everything up to here is done”#

A sequence point is a spot that sets up those relations all at once. In the standard’s words, a sequence point between the evaluations of A and B means every value computation and side effect associated with A is sequenced before every value computation and side effect associated with B (§5.1.2.4p3). Past that spot, the earlier side effects have all finished.

These are all the sequence points the standard lists (Annex C).

WhereSo
between a full expression and the next full expressionthe most important row. The end of the statement x = 1;, the conditions of if, switch, while and do, each of the three parts of for, the expression of return, and each initializer outside a compound literal are full expressions (§6.8.1p4)
between the first and second operands of && and ||the right is looked at after the left’s side effects have finished. Why p && p->n is safe
between the left and right of the comma operatornot the comma that separates arguments — the separator/operator distinction above decides it here
between the condition of ?: and the branch chosenthe branch not chosen is not evaluated
in a function call, after the function designator and arguments are evaluated and before the actual callthe arguments’ side effects are finished when the body is entered. But the order among the arguments is not fixed
immediately before a library function returnsside effects inside a standard function are finished when it returns
after each conversion specifier of formatted I/O, around each call to the comparison function of qsort and bsearchdetailed rules inside printf, scanf and the sort and search functions

Table 52.19 — The complete list of sequence points (C23 Annex C)

There are also orderings that are not sequence points. “Operand value computations → the result’s value computation”, read above (§6.5.1p1); in assignment, “value computations of both sides → the store” (§6.5.17.1p3, chapter 35); in postfix increment, “value computation of the result → the store” (§6.5.3.5p2). These fix the order of two particular events only; they do not promise that any other side effect has finished in between.

52.16.3 The rule that leaves the contract — two unsequenced accesses to one object#

Now the central sentence of the rule.

“ 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.5 ”

Two shapes leave the contract: changing the same object twice without an order, and changing it while reading its value without an order. Judged with the relation table above:

ExpressionVerdict
f() + g()unspecified — the two calls are indeterminately sequenced. Which is called first is not fixed
h(f(), g())unspecified — argument evaluation order
i = i++UB — the two side effects that change i (the assignment and the increment) are unsequenced
i = i++ + 1UB — the same reason
a[i] = i++UB — the side effect of i++ and the value computation of the subscript i are unsequenced
i++ + i++UB
f(i++, i++)UB — the comma separating arguments is not an operator, so there is no sequence point
printf("%d %d", i, i++)UB — the same reason. The read and the change are unsequenced
i++, i++fine — there is a sequence point at the comma operator
(i++) && (i++)fine — there is a sequence point at &&
i = i + 1fine — the read (value computation) is sequenced before the store (§6.5.17.1p3)
x = y = 0fine — the objects changed are different

Table 52.20 — Verdicts on commonly seen expressions

The difference between unspecified and UB becomes visible. This book ran the same code with both compilers.

static int f(void) { puts("f"); return 1; }
static int g(void) { puts("g"); return 2; }
printf("args %d %d\n", f(), g());     /* unspecified */
printf("%d %d\n", i, i++);            /* UB, i is 3 */
Expressiongcc 14clang 19
order printed by printf(…, f(), g())g then f — rightmost argument firstf then g — leftmost first
output of printf("%d %d", i, i++)4 33 3

Table 52.21 — The order the two compilers actually chose (x86-64, -O2)

The first row is unspecified, so both are right. Whatever the order, each function was called exactly once. The second row is UB, so neither is “the right answer” — the same source printed different numbers with different compilers, and with another version or optimisation level it could differ again.

Q. Why go as far as UB? Would “it is computed in one of the two orders”, an unspecified rule, not be enough?

A. If it were unspecified, the compiler would have to guarantee the same result as one of the two orders. But the real use of “unsequenced” is bigger than that. The compiler may interleave the two operands’ computations — for instance, staggering one side’s read and the other side’s write at the instruction level to fill the pipeline or save registers (chapter 13). With two unsequenced writes to the same object, the interleaved result may match neither order — a half-written value may even remain. Rather than define that case, the standard chose to say do not write such expressions.

C++ fixed part of the order in C++17, for instance evaluating the right side of an assignment before the left, turning some of the UB in the “Verdicts on commonly seen expressions” table into defined behaviour. C stays as it was. Code that moves between the two languages has all the more reason not to write such expressions.

The tools catch the common shapes. This is what the two compilers said about the code above.

gcc:   warning: operation on ‘i’ may be undefined [-Wsequence-point]
clang: warning: unsequenced modification and access to 'i' [-Wunsequenced]
clang: warning: unsequenced modification and access to 'i' [-Wunsequenced]

But a shape where whether it is the same object is decided only at run time — two pointers to the same object, as in *p = (*q)++ with p == q — is caught by neither. So the practical rule is the one from chapter 34 — within one full expression, change an object only once, and do not read an object you are changing elsewhere in the same expression. If the order matters, split the statement. The end of a statement is the surest sequence point.

52.17 The grey zones gathered#

Only the operator-related entries, sorted by chapter 56′s three words. The full lists are in Annex J of the standard.

52.17.1 Undefined behaviour (UB)#

placecondition
/ %a zero divisor; INT_MIN / -1, INT_MIN % -1
+ - * ++ --signed integer overflow
<<a count that is negative or at least the width; left-shifting a signed negative; a signed positive whose result does not fit
>>a count that is negative or at least the width
* indirectionnull, an object past its lifetime, a misaligned address, a pointer outside its provenance
[]access outside the array
+ - pointer arithmetica result outside the array (one past the end included)
- between pointerspointers into different arrays
< <= > >=ordering pointers that do not belong to the same array or object
. -> on unionsreading a member other than the one last written (the common initial sequence excepted)
expressions in generalmodifying the same object twice within one sequence point, or modifying it and reading it for another purpose

Table 52.22 — Where an operator’s behaviour is undefined

52.17.2 Unspecified#

placewhat is not settled
subexpressionsthe evaluation order of f() + g()
function argumentsthe order among arguments
== !=whether a one-past-the-end pointer compares equal to a pointer to the next object
padding bytesthe values of a struct’s padding — the reason not to compare with memcmp (chapter 48)

Table 52.23 — The places left unspecified

52.17.3 Implementation-defined#

placewhat the implementation settles
>>the result of shifting a signed negative value (usually an arithmetic shift)
integer conversionthe result of converting a value that does not fit a signed type (still so in C23)
pointer ↔ integerthe result of the conversion and whether it round-trips (only the round trip through uintptr_t, where it exists, is guaranteed)
charsigned or unsigned — it changes the results of >> and of comparisons (chapter 8)
bit-fieldsthe order of allocation and the padding

Table 52.24 — The places the implementation decides

52.18 Things that are not operators#

The same characters appear in the grammar without being operators.

shapewhat it really isin detail
the comma in f(a, b)a separator of the call syntax — no sequence pointchapter 34
the comma in int a, b;a separator of declaration syntaxchapter 24
the comma in {1, 2}a separator in an initialiser listchapters 39 and 47
(type){...}a compound literal — not a cast but syntax that makes an objectchapter 48
the parentheses of sizeof(int)syntax wrapping a type name — not a callchapter 36
# ##preprocessor operators — they act in a different phase of translationchapter 65
the dot in {.x = 1}designated-initialiser syntax, not member accesschapter 47
the star in int *p;declarator syntax, not indirectionchapter 69

Table 52.25 — Things that look like operators but are not

Recap

What to keepThe point
What an expression carriesValue, type, value category, side effects
PrecedenceA grouping rule, not an order of computation
AssociativityWhich side groups first among equals
Where order is guaranteed within an expressionOnly &&, ||, ?:, the comma operator, and function calls
Grey zonesRead UB, unspecified and implementation-defined as distinct words
Working ruleParenthesise, and never touch the same object twice in one expression

Table 52.26 — Expressions and operators — what to remember

You can now read an operator as a contract. The next chapter puts the bitwise ones to work (chapter 53) — the idioms, the traps, and the names C23 gave those idioms. After that come the places where the contracts get subtlest — the mathematics of approximation (chapter 54), handling failure (chapter 55), and what happens when a contract is broken (chapter 56).

Notes

  1. ISO/IEC 9899 (C23) 6.5.1, paragraph 1. ↩
  2. ISO/IEC 9899 (C23) 6.4.6, paragraph 2. ↩
  3. ISO/IEC 9899 (C23) 5.1.2.4, paragraph 2. ↩
  4. ISO/IEC 9899 (C23) 5.1.2.4, paragraph 3. ↩
  5. ISO/IEC 9899 (C23) 6.5.1, paragraph 2. It continues: “If there are multiple allowable orderings of the subexpressions of an expression, the behavior is undefined if such an unsequenced side effect occurs in any of the orderings.” ↩