Skip to main content

Operators, Precedence Traps, and Sequence Points

What This Concept Is

C has roughly 45 operators spread across 15 precedence levels. Two things matter far more than memorizing the table:

  1. Precedence determines how subexpressions group.
  2. Sequence points (in C11 language, the pre/post relationship called sequenced before) determine when side effects are guaranteed to have taken place.

Most "weird C output" bugs come from confusing these, not from a genuine compiler bug.

Important precedence traps:

  • *p++ means *(p++), not (*p)++
  • a & 0xF == 0 means a & (0xF == 0), not (a & 0xF) == 0
  • !a == b means (!a) == b, not !(a == b)
  • Assignment = is low precedence; compare if (x = 5) with if (x == 5)

Sequence-point rules (simplified): within a full expression, if you modify the same object twice without a sequence point between the modifications, or read and modify the same object without a sequence point separating them, the behavior is undefined. ;, &&, ||, ?:, and , (the comma operator) introduce sequencing.

Why It Matters Here

This is where "the code compiles" and "the code does what I meant" diverge. If you are not disciplined about precedence and ordering, two things happen:

  • you write code that works by accident on your compiler and breaks on another
  • you cannot read someone else's code and predict its behavior

Concrete Example

Three innocent-looking snippets:

int a = 1, b = 2;
int c = a < b ? 1 : 2 + 3; /* is this 1 or (2+3) ? */

?: has lower precedence than +, so this parses as a < b ? 1 : (2 + 3). Result: 1. Add parentheses always.

int i = 0;
int x = i++ + i++; /* undefined: two modifications of i, no sequence point */
int y = (i = 1, i + 1); /* defined: comma is a sequence point, y = 2 */
int arr[3] = {10, 20, 30};
int i = 0;
arr[i] = i++; /* undefined: order of read of i vs update of i is unsequenced */

The last two compile clean on -Wall unless you also pass -Wsequence-point, and even then results vary by compiler.

Common Confusion / Misconception

"Parentheses are optional when I know the precedence." They are optional for the compiler. They are mandatory for the next human to read the code. a & MASK == 0 is a bug, not a style opinion.

"The compiler evaluates left to right." Only where the standard says so. Function argument evaluation order is unspecified. f(g(), h()); may call h() before g() with no warning.

How To Use It

Short, safe rules:

  1. Parenthesize any mix of bitwise and relational operators.
  2. Parenthesize macro bodies and macro arguments (see the preprocessor concept).
  3. Do not modify the same variable twice in one expression.
  4. Do not read and write the same variable in the same expression unless a sequence point separates them.
  5. When in doubt, split into two statements.

Check Yourself

  1. Why is a << 2 + 3 almost always wrong?
  2. Give a legal way to swap two ints in one statement (or justify using two statements).
  3. Why is the evaluation order of arguments in printf("%d %d\n", i++, i++); a problem?

Mini Drill or Application

For each snippet, decide whether the expression is defined. If defined, give its value. If undefined, rewrite it as two statements that preserve the intent.

  1. int i = 0; int x = ++i + i++;
  2. int a = 4; int b = a < 3 ? 5 : 6 + 1;
  3. int flags = 0x3; int is_set = flags & 0x1 == 0x1;
  4. int i = 5; int arr[10]; arr[i] = i++;
  5. int x = 1; int y = (x++, x++, x);

Read This Only If Stuck