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.

See Also