Skip to content
C++
Language
Basic

Operator Precedence

Complete C++ operator precedence and associativity reference — all 17 levels with version annotations, pitfalls, and disambiguation techniques.

Operator Precedence

Operator 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.

LevelOperator(s)DescriptionAssociativity
1a::bScope resolutionLeft →
2a++ a--Postfix increment/decrementLeft →
2T(a) T{a}Functional castLeft →
2a()Function callLeft →
2a[]SubscriptLeft →
2a.b a->bMember accessLeft →
3++a --aPrefix increment/decrement← Right
3+a -aUnary plus/minus← Right
3!a ~aLogical NOT, bitwise NOT← Right
3(T)aC-style cast← Right
3*a &aDereference, address-of← Right
3sizeof alignofSize/alignment queries← Right
3co_await aAwait expression (C++20)← Right
3new new[] delete delete[]Dynamic allocation← Right
3^^aReflection operator (C++26)← Right
4a.*b a->*bPointer-to-member accessLeft →
5a*b a/b a%bMultiplicativeLeft →
6a+b a-bAdditiveLeft →
7a<<b a>>bBitwise shiftLeft →
8a<=>bThree-way comparison (C++20)Left →
9a<b a<=b a>b a>=bRelationalLeft →
10a==b a!=bEqualityLeft →
11a&bBitwise ANDLeft →
12a^bBitwise XORLeft →
13a|bBitwise ORLeft →
14a&&bLogical ANDLeft →
15a||bLogical ORLeft →
16a?b:cTernary conditional← Right
16throw co_yield (C++20)Throw/yield expression← Right
16= += -= *= /= %= <<= >>= &= ^= |=Assignment← Right
17a,bCommaLeft →

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:

cpp
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 ==:

cpp
// 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):

cpp
int n = 1 + 2 << 3;   // parsed as (1 + 2) << 3 = 24, not 1 + (2 << 3) = 17

Pointer-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:

cpp
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:

cpp
// 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 multiplied

Ternary 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:

cpp
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

cpp
#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:

cpp
// 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:

cpp
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:

cpp
static_assert((1 + 2 << 3) == 24, "shift is looser than add");
static_assert((1 | 2 & 3) == 3,   "& is tighter than |");   // (1 | (2 & 3)) = 3

Do 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:

cpp
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:

cpp
if (status & (0x01 == 1))  // evaluates (0x01 == 1) -> true -> 1, then status & 1

The fix is explicit parentheses around every bitwise sub-expression:

cpp
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 !=:

cpp
// 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:

cpp
int a = 1 + 2 << 3;   // (1 + 2) << 3 = 24
int b = 1 << 2 + 3;   // 1 << (2 + 3) = 32, NOT (1 << 2) + 3 = 7

The 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:

cpp
// 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) + 1

Always 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:

cpp
// 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:

cpp
// 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:

cpp
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 result

The 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 &&:

cpp
bool in_range = (a < b) && (b < c);   // explicit and correct

This also applies to <=, >, >=, and mixing them:

cpp
// 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 high

If 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:

cpp
// 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:

cpp
// 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 parentheses

Because 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:

cpp
// 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:

cpp
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=3

This 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:

cpp
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:

cpp
// (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.

cpp
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 expression

This 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:

cpp
// 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):

cpp
// 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