52 Expressions and operators
What to know first
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
By the end of this chapter
The questions this chapter answers
- Does an expression without side effects do nothing if you write it? A statement like
x + 1;? - Does “lvalue” simply mean “on the left”?
- Then when does the
i++infor (i = 0; i < n; i++)happen? - What, then, should be used in C?
- 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.
| What | Mathematics | C |
|---|---|---|
| what an expression does | denotes a value | computes a value, designates an object or function, changes something, or several of these together |
| does evaluating change the world | no | it can — x = x + 1 is not an equation but “add one to x”. This is a side effect |
| range of values | numbers have no end | every 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 evaluation | a relation outside time | the 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:
| Branch | Example | Note |
|---|---|---|
| specifies computation of a value | a + b, n * 2, x > 0 | the branch most like mathematics |
| designates an object | x, *p, a[i], s.name | points at a place, not a value — an lvalue (chapter 36) |
| designates a function | printf, main | a function designator: “that function” itself, before it is called |
| generates side effects | x = 1, i++, puts("hi") | what happens besides the computation — next section |
| a combination | i++ (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.
- The same symbol is an operator in one context and not in another. The comma in
f(a, b)is a separator between arguments; the comma in(a, b)is the comma operator. The*ofint *pis part of a declaration; the*of*p = 0is the indirection operator. The=ofint x = 1;marks an initialisation, not the assignment operator. The same look does not bring the same rules — in the sequence point table later, exactly this distinction makes the difference. - There are “other forms of operator”. Operators made of words, like
sizeofandalignof, and operators that come in pairs, like( )(function call),[ ](subscript) and? :(conditional). - Operators are sorted by how many operands they take. One makes it unary (
-x,!b,*p,&x,sizeof x), two binary (a + b,x = y), three ternary. C has exactly one ternary operator,? :.
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 words | Shapes met in this book | Note |
|---|---|---|
| modifying an object | x = 1, x += 2, i++, --n | assignment and increment/decrement are the operators that exist for their side effects (chapter 35) |
| modifying a file | fputs, fprintf, fwrite | screen output too is writing to the standard output stream (chapter 23) |
| a volatile access | reading a hardware register | even a read is a side effect — a device can react to having been read (chapter 114) |
| calling a function that does any of those | printf("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 carries | What that means |
|---|---|
| Value | The result of the computation. 2 + 3 has the value 5 |
| Type | Which container the value sits in — fixed at compile time (chapter 24) |
| Value category | Whether it is an lvalue (it designates a place). x is; x + 1 is not |
| Side effects | Whether 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.
| group | operators | assoc. | why it associates that way |
|---|---|---|---|
| postfix | () [] . -> ++(post) --(post), compound literal | L→R | a.b.c only makes sense burrowing from the left |
| unary | ++ -- + - ! ~ (type) * & sizeof alignof | R→L | the one nearest the operand binds first: - -x, *&x |
| multiplicative | * / % | L→R | the convention of arithmetic |
| additive | + - | L→R | subtraction only makes sense left-associative |
| shift | << >> | L→R | a << 1 << 2 pushes in turn |
| relational | < <= > >= | L→R | which 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→R | short-circuiting only works from the left |
| logical OR | || | L→R | the same reason |
| conditional | ?: | R→L | so a ? b : c ? d : e reads as a ladder |
| assignment | = += -= *= /= %= &= ^= |= <<= >>= | R→L | so that a = b = 0 makes both zero |
| comma | , | L→R | the 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 written | how it really groups | if that was the intent |
|---|---|---|
a & b == c | a & (b == c) | (a & b) == c |
a << 1 + 2 | a << (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 = 0 | a = (b = 0) | (as it is — right-associative) |
x < y < z | (x < y) < z | x < y && y < z |
!x & y | (!x) & y | !(x & y) |
sizeof a + 1 | (sizeof a) + 1 | sizeof(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? | |
|---|---|---|---|
| Operand | a modifiable lvalue of real or pointer type | the same | the same |
| What it does to the object | adds 1 | adds 1 | the same |
| The value of the expression | the value after the change | the value before the change | different |
| Is the result an lvalue | no | no | the same (C++ differs — below) |
| How it is defined | ++E is equivalent to (E += 1) | defined separately | different |
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.
| What | The 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.
| Place | Unoptimised (-O0) | Ordinary build (-O2) |
|---|---|---|
for (…; i++) vs ++i — value unused | the generated assembly does not differ by one byte | the same |
a = (*b)++ vs a = ++(*b) — value used | gcc: 9 instructions vs 11 — the postfix one was the shorter; clang the other way round, the postfix one an instruction longer | 3 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.
| Code | C (GCC) | C++ (G++) |
|---|---|---|
&++x | lvalue required as unary '&' operand — error | accepted |
++x = 5 | lvalue required as left operand of assignment — error | accepted |
&x++ | error | error — 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.
- Operands — what the standard requires. Violate it and the compiler must diagnose it (a constraint violation).
- Result — the type of the value, and whether it is an lvalue.
- Grey zone — in the three words of chapter 56. UB is undefined behaviour, unspecified means one of several possibilities with no rule saying which, and implementation-defined means the implementation chooses and documents it.
- In detail — the chapter that tells the story.
Platform note. What these tables rest on
52.9 Postfix operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
a[i] subscript | one a pointer to a complete object type, the other an integer | an 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(...) call | a function, or a pointer to one | the function’s return type; not an lvalue | unspecified: the order in which arguments are evaluated. UB: arguments that disagree with the prototype | chapters 22, 25 and 67 |
s.m member | a struct or union value and a member name | the member’s type; an lvalue if the left side is one | UB: reading a union member other than the one last written (the common initial sequence is an exception) | chapters 47 and 49 |
p->m member | a pointer to a struct or union, and a member name | the member’s type, an lvalue | UB: a null or otherwise invalid pointer | chapter 47 |
x++ post-increment | a modifiable lvalue of real type (integer or real floating; not complex) or pointer type | the value before the change; not an lvalue | UB: two unsequenced modifications; signed integer overflow | chapter 33 |
x-- post-decrement | the same | the value before the change | the same | chapter 33 |
Table 52.11 — The contract of the postfix operators
52.10 Unary operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
++x --x | a modifiable lvalue of real or pointer type | the value after the change; not an lvalue — C++ differs | ++E is (E += 1) — the overflow rules are the same | chapter 33 |
&x address-of | a function designator, the result of [] or unary *, or an lvalue that is not a bit-field and not declared register | a pointer to it | breaking the constraint is a compile error | chapter 36 |
*p indirection | a pointer type | an lvalue of the pointed-at type | UB: null, an object whose lifetime has ended, a misaligned address, or one outside its provenance | chapters 36, 38 and 45 |
+x | arithmetic type | the promoted value | chapters 21 and 30 | |
-x | arithmetic type | the promoted value | UB: signed integer overflow (-INT_MIN) | chapter 28 |
~x | integer type | the bitwise complement after promotion | it happens at the promoted width — mind narrow types | chapter 29 |
!x | scalar (arithmetic or pointer) | 0 or 1, of type int | chapter 31 | |
(type)x cast | between scalars | a value of that type; not an lvalue | implementation-defined: pointer↔integer conversion. UB: following a misaligned pointer; converting between function and object pointers | chapters 30 and 38 |
sizeof | a complete object type or an expression. Not a function type, an incomplete type, or a bit-field | a size_t value | with a variable length array the operand is evaluated at run time; otherwise it is not evaluated | chapters 36 and 39 |
alignof | the name of a complete object type (not an expression) | a size_t value | chapter 38 |
Table 52.12 — The contract of the unary operators
52.11 Arithmetic operators#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
* multiply | arithmetic types | the common type of the usual arithmetic conversions | UB: signed integer overflow | chapters 28 and 30 |
/ divide | arithmetic types | the same | UB: a zero divisor, INT_MIN / -1 | chapters 29 and 54 |
% remainder | integer types only | the same | UB: a zero divisor, INT_MIN % -1 | chapter 29 |
+ add | both arithmetic, or a pointer to a complete object type and an integer | the common type, or the pointer type | UB: integer overflow; pointer arithmetic beyond the array | chapters 28 and 39 |
- subtract | both arithmetic, a pointer and an integer, or two pointers into the same array | ptrdiff_t for pointer difference | UB: subtracting pointers into different arrays; a difference that does not fit ptrdiff_t | chapters 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#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
E1 << E2 | both of integer type | the type of the promoted left operand | UB: 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 type | chapters 6 and 29 |
E1 >> E2 | both of integer type | the same | UB: E2 negative or at least the width. implementation-defined: the result when E1 is signed and negative | chapters 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#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
< <= > >= | both real types, or two pointers to compatible object types | 0 or 1, of type int | UB: 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 on | 0 or 1, int | unspecified: whether a one-past-the-end pointer compares equal to a pointer to the object that follows | chapters 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#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
& ^ | | both of integer type | the common type of the usual arithmetic conversions | they reach the sign bit, so use them on unsigned types | chapter 29 |
&& | both scalar | 0 or 1, int | guaranteed: if the left is 0 the right is not evaluated, and there is a sequence point between them | chapter 31 |
|| | both scalar | 0 or 1, int | guaranteed: if the left is non-zero the right is not evaluated | chapter 31 |
Table 52.16 — The contract of the bitwise and logical operators
52.15 Conditional, assignment, comma#
| operator | operands | result | grey zone | in detail |
|---|---|---|---|---|
c ? a : b | c scalar. a and b both arithmetic, or compatible structs or unions, or both void, or compatible pointers, or one a null pointer constant | one common type for both branches | guaranteed: a sequence point after the condition; the branch not chosen is not evaluated | chapter 34 |
= | the left must be a modifiable lvalue | the value converted to the left’s type. Not an lvalue | UB: assignment between overlapping objects (exact overlap with compatible types is allowed); two modifications within one sequence point | chapter 24 |
compound op= | E1 op= E2 | as E1 = E1 op E2, except that E1 is evaluated once | the grey zones of the operation itself (overflow, zero divisor) still apply | chapter 34 |
, comma | any two expressions | the type and value of the right | guaranteed: the left is evaluated and discarded, then a sequence point. The comma in an argument list is not this operator | chapter 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 ”
| Relation | Meaning | Typical place |
|---|---|---|
| sequenced before | B starts only after A has finished | left → right of &&, one statement → the next |
| indeterminately sequenced | one finishes before the other starts, but which comes first is unknown | the two function calls in f() + g() |
| unsequenced | no order at all. The two evaluations may even interleave | the 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).
| Where | So |
|---|---|
| between a full expression and the next full expression | the 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 operator | not the comma that separates arguments — the separator/operator distinction above decides it here |
between the condition of ?: and the branch chosen | the branch not chosen is not evaluated |
| in a function call, after the function designator and arguments are evaluated and before the actual call | the arguments’ side effects are finished when the body is entered. But the order among the arguments is not fixed |
| immediately before a library function returns | side 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 bsearch | detailed 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:
| Expression | Verdict |
|---|---|
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++ + 1 | UB — 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 + 1 | fine — the read (value computation) is sequenced before the store (§6.5.17.1p3) |
x = y = 0 | fine — 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 */| Expression | gcc 14 | clang 19 |
|---|---|---|
order printed by printf(…, f(), g()) | g then f — rightmost argument first | f then g — leftmost first |
output of printf("%d %d", i, i++) | 4 3 | 3 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)#
| place | condition |
|---|---|
/ % | 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 |
* indirection | null, an object past its lifetime, a misaligned address, a pointer outside its provenance |
[] | access outside the array |
+ - pointer arithmetic | a result outside the array (one past the end included) |
- between pointers | pointers into different arrays |
< <= > >= | ordering pointers that do not belong to the same array or object |
. -> on unions | reading a member other than the one last written (the common initial sequence excepted) |
| expressions in general | modifying 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#
| place | what is not settled |
|---|---|
| subexpressions | the evaluation order of f() + g() |
| function arguments | the order among arguments |
== != | whether a one-past-the-end pointer compares equal to a pointer to the next object |
| padding bytes | the 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#
| place | what the implementation settles |
|---|---|
>> | the result of shifting a signed negative value (usually an arithmetic shift) |
| integer conversion | the result of converting a value that does not fit a signed type (still so in C23) |
| pointer ↔ integer | the result of the conversion and whether it round-trips (only the round trip through uintptr_t, where it exists, is guaranteed) |
char | signed or unsigned — it changes the results of >> and of comparisons (chapter 8) |
| bit-fields | the 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.
| shape | what it really is | in detail |
|---|---|---|
the comma in f(a, b) | a separator of the call syntax — no sequence point | chapter 34 |
the comma in int a, b; | a separator of declaration syntax | chapter 24 |
the comma in {1, 2} | a separator in an initialiser list | chapters 39 and 47 |
(type){...} | a compound literal — not a cast but syntax that makes an object | chapter 48 |
the parentheses of sizeof(int) | syntax wrapping a type name — not a call | chapter 36 |
# ## | preprocessor operators — they act in a different phase of translation | chapter 65 |
the dot in {.x = 1} | designated-initialiser syntax, not member access | chapter 47 |
the star in int *p; | declarator syntax, not indirection | chapter 69 |
Table 52.25 — Things that look like operators but are not
Recap
| What to keep | The point |
|---|---|
| What an expression carries | Value, type, value category, side effects |
| Precedence | A grouping rule, not an order of computation |
| Associativity | Which side groups first among equals |
| Where order is guaranteed within an expression | Only &&, ||, ?:, the comma operator, and function calls |
| Grey zones | Read UB, unspecified and implementation-defined as distinct words |
| Working rule | Parenthesise, 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
- ISO/IEC 9899 (C23) 6.5.1, paragraph 1. ↩
- ISO/IEC 9899 (C23) 6.4.6, paragraph 2. ↩
- ISO/IEC 9899 (C23) 5.1.2.4, paragraph 2. ↩
- ISO/IEC 9899 (C23) 5.1.2.4, paragraph 3. ↩
- 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.” ↩