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.
Precedence Traps in Depth
Bitwise operators are lower than equality — the mask-test trap
The single most common precedence bug in C and C++ is a flag test like:
uint8_t status = get_status();
// Intended: test whether the READY bit is set and equal to READY
if (status & 0x01 == 1) // BUG
proceed();Because == sits at level 10 and & at level 11 (lower), the compiler parses this as:
if (status & (0x01 == 1)) // evaluates (0x01 == 1) -> true -> 1, then status & 1The fix is explicit parentheses around every bitwise sub-expression:
if ((status & 0x01) == 1) // correct
proceed();
// Even simpler when the mask already IS the expected value:
if (status & 0x01) // checks non-zero — often cleaner
proceed();The same trap affects | (level 13) and ^ (level 12), which are both lower than == and !=:
// Parsed as: a | (b != c) — almost never intended
bool x = a | b != c;
// Correct if you want bitwise OR before comparison:
bool x = (a | b) != c;A good rule: whenever you mix &, ^, or | with any relational or equality operator in a single expression, add parentheses unconditionally.
Shift operators and addition — direction matters
Shift operators live at level 7, between multiplication (level 5) and addition (level 6). This means shift binds looser than both arithmetic operations:
int a = 1 + 2 << 3; // (1 + 2) << 3 = 24
int b = 1 << 2 + 3; // 1 << (2 + 3) = 32, NOT (1 << 2) + 3 = 7The second case is the dangerous one: << to the right of a + absorbs everything to its right, not just the immediate operand. This matters with std::cout:
// This is fine: (cout << a) << b + c
// b + c is evaluated as addition first, then the second <<
std::cout << a << b + c;
// But this silently loses the addition:
int mask = 1 << bit_index + 1; // 1 << (bit_index + 1), not (1 << bit_index) + 1Always parenthesize shift expressions that appear next to additive or multiplicative operators.
The ternary operator: right-associative and very low precedence
?: sits at level 16, just above the comma operator and sharing the level with assignment. It is right-associative. These two facts combine in ways that surprise readers:
// Right-associativity chains ternary correctly:
int sign = x > 0 ? 1 : x < 0 ? -1 : 0;
// = x > 0 ? 1 : (x < 0 ? -1 : 0) — right-associated, intended
// But assignment mixes in at the same level — also right-associative:
// Be careful: a = cond ? b : c is a = (cond ? b : c), which is fine
// But:
int y;
y = flag ? y + 1 : y - 1; // ok: = binds after ?:The low precedence of ?: means you need parentheses whenever it appears as a sub-expression of an operator that you expect to be at the same level or below:
// Without parens: cond ? a : b + 1 is cond ? a : (b + 1) — + is tighter
// That is usually what you want, but confirm it by reading left to right.
// A common error: mixing ternary into bitwise
uint8_t flags = condition ? FLAG_A : FLAG_B | FLAG_C;
// Parsed as: condition ? FLAG_A : (FLAG_B | FLAG_C)
// Which may be intended — but only if you want both FLAG_B and FLAG_C when false.
// If you intended (condition ? FLAG_A : FLAG_B) | FLAG_C, you need parens.Comparison Chaining: a < b < c Is Not a Range Check
Unlike Python, C++ does not chain relational operators into a single range predicate. a < b < c is legal C++ but does not test whether b lies between a and c:
int a = 3, b = 5, c = 2;
// Parsed as (a < b) < c
// Step 1: (3 < 5) -> true -> promoted to int 1
// Step 2: 1 < 2 -> true
// Result: true — but 5 is NOT between 3 and 2!
bool wrong = a < b < c; // compiles, produces misleading resultThe compiler does not warn about this because it is syntactically and semantically valid — it compares a bool (promoted to int) against c. The correct C++ range check uses &&:
bool in_range = (a < b) && (b < c); // explicit and correctThis also applies to <=, >, >=, and mixing them:
// Each of these compiles but does NOT test what it looks like:
bool x = 0 <= value <= 100; // (0 <= value) <= 100 — always true when 100 >= 1
bool y = low < mid > high; // (low < mid) > high — compares bool to highIf you receive a signed comparison warning on the second comparison in a chain, that is a hint the compiler has seen a bool-to-integer promotion — a reliable signal that the expression is chained incorrectly.
Overloaded Operator Precedence
When you define operator+, operator&, or any other operator for a user-defined type, the new operator inherits the precedence and associativity of its built-in counterpart. The C++ standard does not provide any mechanism to alter this: you cannot make a user-defined operator+ bind tighter than operator*.
This has significant implications for embedded DSLs built on operator overloading:
// Expression-template library using overloaded operators:
auto expr = a + b * c; // * still binds tighter than +, even for user types
auto bits = flags & mask == expected; // & still lower than == — the same trap!The trap is that DSL code looks like natural notation but still obeys C++ precedence. A user of a matrix or parser-combinator library who forgets the table will introduce bugs:
// Parser combinator: | is "choice", >> is "sequence"
// But | is level 13 and >> is level 7 — so >> binds tighter than |
auto rule = token_a >> token_b | token_c;
// Parsed as (token_a >> token_b) | token_c — sequence then choice
// If you wanted token_a >> (token_b | token_c), you need parenthesesBecause you cannot change precedence, well-designed C++ DSLs document their operator precedence explicitly and encourage liberal use of parentheses for non-obvious groupings.
The Comma Operator
The comma operator (level 17) is the lowest-precedence operator in C++. It evaluates its left operand, discards the result, then evaluates and returns its right operand. It is left-associative.
Its primary legitimate use is the increment expression of a for loop, where it advances multiple variables simultaneously:
// Advance two iterators in lockstep
for (auto it1 = v1.begin(), it2 = v2.begin();
it1 != v1.end();
++it1, ++it2) // comma operator: ++it1 then ++it2
{
process(*it1, *it2);
}Outside for-loop increment clauses, the comma operator is almost never the right tool. Its most dangerous misuse is in function call argument lists, where , between arguments is syntax (an argument separator), not the comma operator. Adding extra parentheses to create a comma-operator expression merely changes the argument count:
void f(int x, int y);
f(1, 2); // two arguments: x=1, y=2 — correct
f(1, (2, 3)); // still two arguments: x=1, y=(2,3) evaluates to 3
// (2,3) is the comma operator: discard 2, return 3
// f(1, 2, 3) would be a compile error (too many arguments)
// f((1, 2), 3) is two arguments: x=(1,2)=2, y=3This is a frequent macro pitfall: if a macro parameter contains a comma, unparenthesized it splits across arguments. Always wrap multi-expression macro arguments in parentheses.
The comma operator also appears in contexts where a sequence of side effects must fit in a single expression — for instance, inside static_assert or a conditional expression — but modern C++ rarely needs this; a lambda invoked immediately is clearer.
sizeof and decltype Precedence
sizeof binds to its immediate operand only
sizeof is a prefix unary operator at level 3 — it binds very tightly. When written without parentheses on an expression, it only captures the immediately adjacent operand:
int x = 4;
int y = 2;
// sizeof x + y is (sizeof x) + y = 4 + 2 = 6
// NOT sizeof(x + y) = sizeof(int) = 4
std::size_t a = sizeof x + y; // 6
// To measure the size of the sum's type:
std::size_t b = sizeof(x + y); // 4 (parentheses needed)When a type name is the operand, parentheses are required by the grammar — sizeof(int) — but this looks like a function call. The parentheses belong to the type-id syntax, not to sizeof itself:
// (sizeof(int)) * p — sizeof applied to int, result multiplied by p
// NOT sizeof((int)*p) — this would measure the size of a dereferenced cast
std::size_t s = sizeof(int) * p;The safe habit: always parenthesize sizeof arguments, both types and expressions, to eliminate all ambiguity.
decltype with expressions vs. parenthesized expressions
decltype has similar concerns. decltype(e) and decltype((e)) have deliberately different semantics — a single extra layer of parentheses changes the result from the declared type to a reference type — but decltype itself is not an operator and does not participate in precedence calculations; it is always followed by a parenthesised argument.
int x = 0;
decltype(x) y = 0; // int — type of the variable x
decltype((x)) z = x; // int& — lvalue reference, because (x) is an lvalue expressionThis is not a precedence issue per se, but it is a grammatical trap in the same family: the number of parentheses changes the meaning, and the rule is non-obvious.
Assignment in Conditions
The idiom of placing an assignment inside a condition is well-defined and idiomatic when used carefully. Because assignment (level 16) is lower precedence than almost every other operator, the assignment is the last thing that "binds" before the condition value is read:
// Read chunks until read() returns 0 (EOF or error)
int n;
while ((n = read(fd, buf, sizeof buf)) > 0) {
process(buf, n);
}The outer parentheses around n = read(...) are mandatory here: without them, > (level 9) would bind before = (level 16), and the compiler would parse n = (read(...) > 0), assigning a bool to n and looping forever or immediately.
C++ also allows declaring a variable inside the condition of while, if, or for (since C++17 for if/switch):
// Variable declared in condition — scoped to the loop body
while (auto line = read_line(file)) {
process(*line);
}
// if with initializer (C++17)
if (auto it = map.find(key); it != map.end()) {
use(it->second);
}In these declaration forms, the value of the declared variable is converted to bool to form the condition. This is not a precedence interaction — the grammar handles it separately — but it achieves the same "assign then test" pattern with better scoping.
Warning: the unparenthesised = in a plain boolean condition generates a compiler warning with -Wall (-Wparentheses specifically). The parenthesised form while ((n = f())) suppresses it, which is why you see the double parentheses in production code — they are a signal to the compiler (and the reader) that the assignment is intentional.
See Also
- Operator Overloading — user-defined operators follow the same precedence table as built-in operators
- Spaceship Operator —
<=>at precedence level 8 (C++20)