Operator Precedence
Complete C++ operator precedence and associativity reference — all 17 levels with version annotations, pitfalls, and disambiguation techniques.
Operator PrecedenceOperator precedence defines the order in which operators bind to their operands when an expression contains multiple operators without explicit parentheses; associativity resolves ties among operators at the same precedence level.
Overview
C++ defines 17 precedence levels. Higher-numbered rows in the table below bind more loosely — the scope resolution operator :: at level 1 binds tightest; the comma operator at level 17 binds loosest.
| Level | Operator(s) | Description | Associativity |
|---|---|---|---|
| 1 | a::b | Scope resolution | Left → |
| 2 | a++ a-- | Postfix increment/decrement | Left → |
| 2 | T(a) T{a} | Functional cast | Left → |
| 2 | a() | Function call | Left → |
| 2 | a[] | Subscript | Left → |
| 2 | a.b a->b | Member access | Left → |
| 3 | ++a --a | Prefix increment/decrement | ← Right |
| 3 | +a -a | Unary plus/minus | ← Right |
| 3 | !a ~a | Logical NOT, bitwise NOT | ← Right |
| 3 | (T)a | C-style cast | ← Right |
| 3 | *a &a | Dereference, address-of | ← Right |
| 3 | sizeof alignof | Size/alignment queries | ← Right |
| 3 | co_await a | Await expression (C++20) | ← Right |
| 3 | new new[] delete delete[] | Dynamic allocation | ← Right |
| 3 | ^^a | Reflection operator (C++26) | ← Right |
| 4 | a.*b a->*b | Pointer-to-member access | Left → |
| 5 | a*b a/b a%b | Multiplicative | Left → |
| 6 | a+b a-b | Additive | Left → |
| 7 | a<<b a>>b | Bitwise shift | Left → |
| 8 | a<=>b | Three-way comparison (C++20) | Left → |
| 9 | a<b a<=b a>b a>=b | Relational | Left → |
| 10 | a==b a!=b | Equality | Left → |
| 11 | a&b | Bitwise AND | Left → |
| 12 | a^b | Bitwise XOR | Left → |
| 13 | a|b | Bitwise OR | Left → |
| 14 | a&&b | Logical AND | Left → |
| 15 | a||b | Logical OR | Left → |
| 16 | a?b:c | Ternary conditional | ← Right |
| 16 | throw co_yield (C++20) | Throw/yield expression | ← Right |
| 16 | = += -= *= /= %= <<= >>= &= ^= |= | Assignment | ← Right |
| 17 | a,b | Comma | Left → |
Precedence is a compile-time grammatical property, not a runtime evaluation order guarantee. The order in which operands are evaluated is a separate question governed by sequencing rules.
Common Pitfalls
Bitwise operators below comparison operators
Precedence levels 9–13 are a chronic source of bugs. Bitwise AND (&, level 11) binds looser than equality (==, level 10), so a flag test written without parentheses does the wrong thing:
uint32_t flags = 0x05;
// Bug: parsed as flags & (value == 1), not (flags & mask) == 1
if (flags & 0x04 == 1) // wrong
handle();
// Correct
if ((flags & 0x04) == 1)
handle();Similarly, bitwise OR (|, level 13) binds looser than ==:
// Parsed as a | (b == c), almost certainly wrong
int x = a | b == c;
// Almost certainly intended:
int x = a | (b == c ? 1 : 0);Shift operators and arithmetic
Shift operators (level 7) bind looser than multiplication (level 5) but tighter than addition (level 6):
int n = 1 + 2 << 3; // parsed as (1 + 2) << 3 = 24, not 1 + (2 << 3) = 17Pointer-to-member and dereference
The pointer-to-member operators .* and ->* (level 4) bind tighter than multiplication but looser than postfix operators. Forgetting this leads to misreads:
struct Foo { int value; };
int Foo::* pm = &Foo::value;
Foo obj;
// ->* binds tighter than + so this reads as: (obj.*pm) + 1
int r = obj.*pm + 1;sizeof is not a function call
sizeof is a unary prefix operator at level 3. A parenthesized type argument looks like a function call but parentheses here belong to the type syntax, not sizeof:
// sizeof(int) * p is (sizeof(int)) * p, NOT sizeof((int)*p)
// sizeof binds to the parenthesized type name only
std::size_t s = sizeof(int) * p; // s = sizeof(int), then multipliedTernary and assignment are right-associative
Both sit at level 16 and associate right-to-left, so chained assignments work as expected, and mixing ternary with assignment requires care:
int a, b, c;
a = b = c = 0; // parsed as a = (b = (c = 0)) — correct
// Ternary is right-associative too:
int x = cond ? val1 : other_cond ? val2 : val3;
// = cond ? val1 : (other_cond ? val2 : val3)Examples
Flag manipulation with explicit grouping
#include <cstdint>
enum class Perm : uint8_t {
Read = 0x01,
Write = 0x02,
Execute = 0x04,
};
bool can_write_and_execute(uint8_t perms) {
// Without parens: & binds tighter than |, so this is correct as written,
// but make intent explicit for readers.
return (perms & static_cast<uint8_t>(Perm::Write)) &&
(perms & static_cast<uint8_t>(Perm::Execute));
}Chained relational expressions (C++20 three-way)
Before C++20, a < b < c compiles but compares (a < b) (a bool, promoted to 0 or 1) against c — rarely what you want. C++20's <=> (level 8) lets library authors implement total ordering cleanly, but chaining relational operators in user code still requires explicit logic:
// C++20
#include <compare>
struct Version {
int major, minor, patch;
auto operator<=>(const Version&) const = default; // C++20
};
// Three-way comparison at level 8, relational at level 9
// So (v1 <=> v2) < 0 parses as ((v1 <=> v2) < 0) — correct
bool older(Version v1, Version v2) {
return (v1 <=> v2) < 0;
}Logical short-circuit with side effects
&& (level 14) and || (level 15) are left-associative and short-circuit. Precedence determines grouping; sequencing rules guarantee the left operand is fully evaluated (and its side effects visible) before the right operand is evaluated:
bool open_and_read(File& f) {
// f.open() is always called; f.read() only if open() returns true
return f.open() && f.read();
}Best Practices
Parenthesize any expression mixing bitwise and logical operators. The gap between levels 11–13 (bitwise) and 14–15 (logical) is wide enough that silent misreads are common, especially with & near ==.
Treat comma as a sequencing operator, not a separator, in expression context. In function call arguments or initializer lists, , is syntax; in a raw expression it is the comma operator (level 17). This distinction matters for macros and lambda bodies.
Use static_assert or compile-time evaluation to validate precedence assumptions. If a constant expression produces the expected value, you have machine-checked documentation:
static_assert((1 + 2 << 3) == 24, "shift is looser than add");
static_assert((1 | 2 & 3) == 3, "& is tighter than |"); // (1 | (2 & 3)) = 3Do not rely on precedence to express non-obvious intent. Parentheses cost nothing at runtime; omitting them when the result surprises readers is a maintenance liability.
See Also
- Operator Overloading — user-defined operators follow the same precedence table as built-in operators
- Spaceship Operator —
<=>at precedence level 8 (C++20)