What Are Operator Precedence and Associativity?

Precedence and associativity are the two rules a C++ compiler uses to decide which operand belongs to which operator when one expression contains more than one operator.

They are parsing rules. Long before your program runs, the compiler reads a line such as logsMilled * planksPerLog - rejectedPlanks and has to settle a structural question: does planksPerLog belong to the * on its left, or to the - on its right? Precedence answers that when the competing operators are different ones. Associativity answers it when they are the same one.

Notice what that question is not about. It is not about which variable gets fetched first, or which function call happens first. That is a completely separate question with a genuinely surprising answer, and the last part of this lesson is devoted to it.

Key Concept
Precedence and associativity build the shape of an expression. They do not schedule the work inside it.

Precedence: Which Operator Claims the Operand

Every C++ operator carries a precedence level, conventionally numbered from 1 to 17. Level 1 binds most tightly and level 17 most loosely, so when two different operators compete for the same operand, the one with the lower level number takes it.

Multiplication, division and remainder sit at level 5. Addition and subtraction sit at level 6. Level 5 is tighter, so a sawmill plank count comes out exactly the way ordinary arithmetic would suggest:

#include <iostream>

int main()
{
    constexpr int logsMilled{6};
    constexpr int planksPerLog{3};
    constexpr int rejectedPlanks{4};

    std::cout << "as written:      " << logsMilled * planksPerLog - rejectedPlanks << '\n';
    std::cout << "grouped for you: " << (logsMilled * planksPerLog) - rejectedPlanks << '\n';
    std::cout << "the other way:   " << logsMilled * (planksPerLog - rejectedPlanks) << '\n';

    return 0;
}

This outputs:

as written:      14
grouped for you: 14
the other way:   -6

The first two lines print the same number because they are the same expression: the brackets on the second line are precisely the brackets precedence had already supplied on the first. The third line is the grouping you would have to ask for explicitly, and it shows how far off the answer lands if you guess wrong about which operator claimed planksPerLog.

Associativity: The Tie-Break When Precedence Ties

Precedence is no help at all with 40 - 12 - 5. Both operators are -, both sit at level 6, and neither can outrank the other. Something else has to break the tie, and that something is associativity: every precedence level is labeled either left to right or right to left.

Level 6 is left to right, so the leftmost - claims its operands first. Level 16, which holds the assignment operators, is right to left, so there the rightmost = claims its operands first.

#include <iostream>

int main()
{
    std::cout << "40 - 12 - 5   -> " << 40 - 12 - 5 << '\n';
    std::cout << "(40 - 12) - 5 -> " << (40 - 12) - 5 << '\n';
    std::cout << "40 - (12 - 5) -> " << 40 - (12 - 5) << '\n';

    std::cout << "48 / 4 / 2    -> " << 48 / 4 / 2 << '\n';
    std::cout << "(48 / 4) / 2  -> " << (48 / 4) / 2 << '\n';
    std::cout << "48 / (4 / 2)  -> " << 48 / (4 / 2) << '\n';

    int eastBench{0};
    int westBench{0};
    int southBench{0};

    eastBench = westBench = southBench = 5;
    std::cout << "chained assignment -> " << eastBench << ' ' << westBench << ' ' << southBench << '\n';

    return 0;
}

This outputs:

40 - 12 - 5   -> 23
(40 - 12) - 5 -> 23
40 - (12 - 5) -> 33
48 / 4 / 2    -> 6
(48 / 4) / 2  -> 6
48 / (4 / 2)  -> 24
chained assignment -> 5 5 5

Subtraction and division agree with their left-to-right partner every time, and disagree with the right-to-left version. The chained assignment reaches all three benches only because level 16 runs the other way: it groups as eastBench = (westBench = (southBench = 5)), so 5 lands in southBench first and the result flows leftwards. Written out left to right as ((eastBench = westBench) = southBench) = 5, it still compiles, but only eastBench ends up holding 5.

A Precedence Table for the Operators in This Course

Below is a reference table you can come back to. Level 1 binds tightest, level 17 loosest, and every operator on a single row shares that row's grouping direction.

Level Operators Grouping direction A grouping it forces
2 () grouping and function call, x++, x--, static_cast<T>() left to right static_cast<double>(a) / b groups as (static_cast<double>(a)) / b
3 unary +, unary -, ++x, --x, !, sizeof right to left -planks + 4 groups as (-planks) + 4
5 *, /, % left to right a * b / c groups as (a * b) / c
6 +, - left to right a - b - c groups as (a - b) - c
7 <<, >> left to right std::cout << a << b groups as (std::cout << a) << b
9 <, <=, >, >= left to right a < b == c groups as (a < b) == c
10 ==, != left to right a == b != c groups as (a == b) != c
14 && left to right a && b || c groups as (a && b) || c
15 || left to right a || b && c groups as a || (b && c)
16 ?:, =, +=, -=, *=, /=, %= right to left total = a + b groups as total = (a + b)
17 , left to right total = a, b groups as (total = a), b

Two things worth reading off the table right now. First, && at level 14 outranks || at level 15, which settles the classic question of how a && b || c groups. Second, the assignment operators at level 16 are beaten only by the comma operator at 17, which is why everything to the right of a lone = is finished before the assignment happens.

The gaps in the numbering are deliberate. Levels 1, 4, 8, 11, 12 and 13 hold ::, .*, ->*, <=>, and the bitwise &, ^ and |, all of which need language features this course has not reached yet. Several operators listed above are also still ahead of you: % and ++ arrive later in this chapter, along with the comparison, logical and conditional operators. The table is here so you can return to it, not so you can memorize it today.

Warning
There is no exponentiation operator in C++. `^` is bitwise exclusive-or, so base ^ 2 compiles happily and computes something you did not want. Raising a number to a power is covered in the Remainder and Exponentiation lesson.

The Trap Sitting at Level 7

Level 7 is the one that bites beginners, because << is both the bitwise shift operator and the stream insertion operator, and it binds tighter than every comparison. That means a comparison written inline in an output statement gets torn apart. This program is deliberately broken:

#include <iostream>

int main()
{
    constexpr int plankMeters{4};
    constexpr int targetMeters{4};

    std::cout << "on target: " << plankMeters == targetMeters << '\n';

    return 0;
}

The compiler groups it as ((std::cout << "on target: ") << plankMeters) == (targetMeters << '\n'), so it ends up asked to compare a stream against an integer:

/tmp/s.cpp: In function 'int main()':
/tmp/s.cpp:8:47: error: no match for 'operator==' (operand types are 'std::basic_ostream<char>' and 'int')
    8 |     std::cout << "on target: " << plankMeters == targetMeters << '\n';
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^~ ~~~~~~~~~~~~~~~~~~~~
      |                                |                              |
      |                                std::basic_ostream<char>       int

(The real diagnostic then lists sixteen candidate overloads; the lines above are the part that matters.) The fix is a pair of parentheses: std::cout << "on target: " << (plankMeters == targetMeters) << '\n';.

Parentheses Outrank Everything

Grouping parentheses sit at level 2, near the top of the table, so anything you wrap is settled before the operators around it get a look in. That gives you a way to state your intent instead of trusting the reader to recall seventeen precedence levels.

#include <iostream>

int main()
{
    constexpr int crateCount{7};
    constexpr int planksPerCrate{9};
    constexpr int splitPlanks{5};

    int shiftTotal{0};

    shiftTotal = crateCount * planksPerCrate - splitPlanks;
    std::cout << "written plainly: " << shiftTotal << '\n';

    shiftTotal = ((crateCount * planksPerCrate) - splitPlanks);
    std::cout << "fully bracketed: " << shiftTotal << '\n';

    return 0;
}

This outputs:

written plainly: 58
fully bracketed: 58

Both statements produce 58, and the bracketed one is arguably harder to read, which is the point: parentheses are worth adding where they resolve a genuine doubt, not everywhere.

Best Practice
Parenthesize any compound expression whose grouping you would have to look up, even when the parentheses are technically unnecessary. Plain arithmetic built from `+`, `-`, `*` and `/` follows school rules and can be left bare; anything mixing those with comparisons, logic or shifts should be spelled out.
Best Practice
Leave the right side of a lone assignment bare. One = and no comma in the statement means level 16 has already grouped everything to its right first, so shiftTotal = crateCount * planksPerCrate - splitPlanks; needs nothing added to it. Brackets start earning their keep again the moment a second assignment joins in, as in shiftTotal = (crateCount *= 2);.

Grouping Is Not the Same as Running Order

Here is the part that catches out experienced programmers, not just beginners.

The C++ standard separates two ideas that everyday speech runs together. Value computation is the act of applying an operator to produce a result, and precedence and associativity really do fix the order of that. Evaluation is the act of working out what an operand is worth, and precedence and associativity say nothing whatsoever about that.

Take logsMilled * planksPerLog - rejectedPlanks again. Grouping guarantees that the multiplication's value computation finishes before the subtraction's, because the subtraction needs its result. It guarantees nothing about when logsMilled, planksPerLog or rejectedPlanks are read. The compiler may fetch them in any order it likes, and for plain variables that is fine, because reading a variable changes nothing.

It stops being fine the moment an operand does something. The program below is deliberately broken:

#include <iostream>
#include <string_view>

int readTally(std::string_view bench)
{
    std::cout << "Planks stacked at the " << bench << " bench: ";
    int tally{};
    std::cin >> tally;
    return tally;
}

int main()
{
    int benchGap{readTally("east") - readTally("west")};
    std::cout << "east minus west: " << benchGap << '\n';

    return 0;
}

It compiles without a single warning, which is part of the problem. Grouping is not in doubt: the left call is the left operand of - and the right call is the right operand. What is in doubt is which call runs first. The standard leaves the order of a subtraction's operands unspecified, so a conforming compiler may issue either call first.

That choice shows up twice over. The two prompts can appear in either order, so you cannot predict which bench the program asks about first. And whichever call runs first swallows the first number typed, so entering 12 and then 8 gives 4 under one compiler and -4 under another. Neither compiler is wrong and neither answer is more correct. Any single compiler picks an order and sticks to it, which is exactly what makes this dangerous: the program looks perfectly stable right up until you switch compiler, raise the optimization level, or edit a nearby line.

The repair is not more parentheses. It is to stop putting two order-sensitive calls in one expression:

#include <iostream>
#include <string_view>

int readTally(std::string_view bench)
{
    std::cout << "Planks stacked at the " << bench << " bench: ";
    int tally{};
    std::cin >> tally;
    return tally;
}

int main()
{
    int eastTally{readTally("east")};
    int westTally{readTally("west")};

    int benchGap{eastTally - westTally};
    std::cout << "east minus west: " << benchGap << '\n';

    return 0;
}

Statements are sequenced: the first finishes before the second starts. Entering 12 and then 8 now produces:

Planks stacked at the east bench: Planks stacked at the west bench: east minus west: 4

and it will produce that on every conforming compiler, forever.

Warning
Never write an expression whose result depends on the order its operands or function arguments are evaluated in. The compiler will not warn you, the program will appear to work, and it will change its answer on somebody else's machine.
Warning
Parentheses do not fix this. They control grouping only. In (readTally("east") - readTally("west")) * 2 the brackets change nothing about which call runs first.

The Short List of Operators That Do Promise an Order

A handful of operators are explicitly sequenced by the standard. C++17 grew that list noticeably, so older advice on this topic is often out of date.

Operator What the language promises Since
&&, || the left operand is fully evaluated first, and the right one may be skipped altogether always
, (the comma operator) the left operand is fully evaluated first, and its value is discarded always
?: the condition is evaluated first, then exactly one of the two branches always
<<, >> the left operand is fully evaluated before the right one C++17
=, +=, -=, *=, /=, %= the right operand is evaluated before the left one C++17

The shift row is the one you can watch working, because every output statement you write is a chain of <<:

#include <iostream>
#include <string_view>

int benchTally(std::string_view bench, int planks)
{
    std::cout << "counting the " << bench << " bench" << '\n';
    return planks;
}

int main()
{
    std::cout << benchTally("east", 12) << '\n' << benchTally("west", 8) << '\n';

    return 0;
}

This outputs:

counting the east bench
12
counting the west bench
8

The chain groups as (((std::cout << benchTally("east", 12)) << '\n') << benchTally("west", 8)) << '\n', and because the left operand of << is sequenced before the right one, the east bench is counted and printed before the west bench is even looked at. Before C++17 a chain like this was free to run the calls the other way round. Compilers often chose left to right anyway, but only C++17 turned that habit into a promise you can build on.

Function arguments did not join this list. In printGap(readTally("east"), readTally("west")) the two calls still run in an unspecified order, exactly as they do either side of a -.

Summary

  • Precedence ranks operators from level 1 (tightest) to level 17 (loosest). When different operators compete for an operand, the lower level number wins it. *, / and % are level 5; + and - are level 6, which is why 6 + 3 * 4 groups as 6 + (3 * 4) and gives 18
  • Associativity breaks the tie when two operators share a level. Most levels are left to right, so 12 - 5 - 2 groups as (12 - 5) - 2 and gives 5. The unary operators at level 3 and the assignment operators at level 16 run right to left
  • Parentheses sit at level 2 and override both rules. Use them wherever the grouping would otherwise need a lookup
  • Assignment is level 16, beaten only by the comma operator at 17, so an expression with a single assignment needs no parentheses around its right operand
  • << is level 7 and outranks every comparison, so a comparison inside an output statement must be bracketed
  • Value computation is applying an operator; precedence and associativity fix its order. Evaluation is working out what an operand is worth; precedence and associativity have no say over its order
  • Operand and function-argument evaluation order is unspecified in most expressions. Split order-sensitive calls into separate statements instead of relying on it
  • The exceptions are short and worth knowing: &&, ||, , and ?: have always evaluated their left operand first, and C++17 added left-to-right sequencing for << and >> and right-then-left sequencing for the assignment operators