C++ / OPERATORS AND EXPRESSIONS
Operator precedence and the bugs parentheses prevent
Predict how C++ groups an unparenthesized expression, and add the parentheses that stop misparsed ternaries, shifts and bitwise tests.
What you will learn
- Read an expression as the tree the compiler builds, not as words from left to right
- Know that & ^ | bind looser than ==, and that ?: = and , bind loosest of all
- Parenthesize comparisons and conditionals before feeding them to <<
- Turn on -Wall so -Wparentheses catches n & 1 == 0 style misgroupings
Understanding Operator precedence and the bugs parentheses prevent
Precedence is a property of the C++ grammar, not of the values involved. Before anything runs, the compiler turns your tokens into one expression tree: precedence decides which operator gets which operands, and associativity only breaks ties between operators on the same level. A useful mental model is that the compiler inserts a complete set of parentheses for you, and every precedence bug is a case where its parentheses differ from the ones in your head. Writing them yourself costs nothing at runtime, because the generated code depends on the tree, not on how the tree was spelled.
Nearly all real misgroupings come from the bottom of the table. The conditional operator, assignment and the comma operator bind looser than everything arithmetic, so total - member ? 20 : 0 first computes total - member and then chooses between 20 and 0. The operators & ^ | bind looser than == and != for a historical reason rather than a logical one: in early C, & and | were used as the logical connectives before && and || existed, and changing their level later would have broken working code, which is why flags & MASK == 0 quietly tests MASK == 0. And << binds tighter than every comparison, which matters constantly because stream insertion is that same operator.
Two things precedence does not do. It does not fix evaluation order: f() + g() * h() is grouped as f() + (g() * h()), yet the three calls may run in any order, and only some operators, among them assignment, << and >>, [] and .*, were given a guaranteed order in C++17. It also cannot be redefined by overloading, so your own operator* for a matrix type still binds tighter than your own operator+, and there is no syntax that changes that. The only tools you have for grouping are parentheses and the habit of naming a subexpression once the tree grows past two levels.
<iostream>
int main() {
int base = 100;
int qty = 12;
bool member = true;
// Meant: 100 + 12 * 5, then 20 off for a member.
int wrong = base + qty * 5 - member ? 20 : 0;
int fixed_total = base + qty * 5 - (member ? 20 : 0);
// Meant: the value of bit 2, plus 3.
int shift_wrong = 1 << 2 + 3;
int shift_fixed = (1 << 2) + 3;
std::cout << "wrong total = " << wrong << '\n';
std::cout << "fixed total = " << fixed_total << '\n';
std::cout << "1 << 2 + 3 = " << shift_wrong << '\n';
std::cout << "(1 << 2) + 3 = " << shift_fixed << '\n';
}
Precedence and associativity are grammar rules that fix how an expression is grouped into a tree before anything is evaluated, and neither your intent nor operator overloading can change that grouping.
Worked examples
A conditional streamed without parentheses
Shows why std::cout << (c) ? a : b prints a 1 and throws both strings away.
<iostream>
int main() {
int temp = 30;
std::cout << (temp > 25) ? "hot\n" : "mild\n"; // the stream is the condition
std::cout << '\n';
std::cout << ((temp > 25) ? "hot\n" : "mild\n");
}
Example explained
Line 1Insertion binds tighter than ?:, so the first statement groups as (std::cout << (temp > 25)) ? "hot\n" : "mild\n".
Line 2std::ostream has an explicit operator bool and a ?: condition is contextually converted, so this misparse compiles rather than being rejected.
Line 3The stream is in a good state, so the condition is true, "hot\n" is selected as the value of the expression and then discarded, and nothing prints it.
Line 4Wrapping the whole conditional makes it one operand of <<, which is why the last statement prints hot.
An even test that is always false
Demonstrates that == binds tighter than &, so the mask is applied to the comparison result.
<iostream>
int main() {
int n = 6;
if (n & 1 == 0)
std::cout << "buggy: n is even\n";
else
std::cout << "buggy: n is odd\n";
if ((n & 1) == 0)
std::cout << "fixed: n is even\n";
else
std::cout << "fixed: n is odd\n";
}
Example explained
Line 1The first condition groups as n & (1 == 0), that is 6 & 0, which is 0 and therefore false.
Line 2The comparison result converts to int 0 before & sees it, so the code is well formed and fails silently instead of crashing.
Line 3(n & 1) == 0 masks first and then compares, giving 0 == 0, so the second test correctly reports even.
Line 4Both GCC and Clang flag this shape under -Wparentheses as a comparison in the operand of &.
Associativity, not just precedence
Shows how right associativity makes a ternary ladder work and left associativity fixes the meaning of repeated subtraction.
<iostream>
int main() {
int score = 74;
const char* grade = score >= 90 ? "A"
: score >= 80 ? "B"
: score >= 70 ? "C"
: "F";
std::cout << grade << '\n';
std::cout << 100 - 20 - 5 << '\n';
}
Example explained
Line 1?: is right-associative, so the ladder groups as score >= 90 ? "A" : (score >= 80 ? "B" : (score >= 70 ? "C" : "F")).
Line 2Each comparison binds tighter than ?:, which is why score >= 70 needs no parentheses of its own.
Line 3Subtraction is left-associative, so 100 - 20 - 5 is (100 - 20) - 5 = 75, not 100 - (20 - 5) = 85.
Line 4Additive operators bind tighter than <<, so the stream receives the single value 75.
Important notes
Clang warns that ?: has lower precedence than the arithmetic beside it, while GCC often stays quiet about that shape, so a clean build is not proof that the grouping is the one you intended.
Overloading inherits precedence and associativity from the built-in operator: for your own vector type a + b * 2 still groups as a + (b * 2), and std::cout << x + y still adds before it prints.
Common mistakes
Writing if (flags & MASK == 0): it groups as flags & (MASK == 0), so the condition is flags & 0, which is false for every input and the branch never runs.
Streaming a conditional unwrapped: std::cout << (n > 0) ? "+" : "-" prints 1 and discards both strings, and dropping the inner parentheses instead gives a compile error about comparing an ostream with an int.
Assuming precedence also fixes evaluation order: f() + g() * h() is grouped as f() + (g() * h()), but the three calls may run in any order, so code that depends on that order can change behaviour between compilers.
Try it yourself
Change, predict, then run
In a browser editor, write down your predicted grouping of 2 + 3 * 4 % 5 << 1, then print both that raw expression and your fully parenthesized version. Adjust your parentheses until the two values match, and explain which operator surprised you.
Open the C++ workspaceCheck your understanding
Given int n = 5; what does the statement std::cout << n & 1; do?
- Nothing runs: it fails to compile, because it groups as (std::cout << n) & 1 and no operator& accepts an ostream and an int
- It prints 1, because & binds tighter than << so the value 5 & 1 is streamed
- It prints 5, because the & 1 applies to the stream result and is ignored
- It prints 51, because n is streamed first and then & streams the 1
Show answer
Stream insertion is the shift operator, which binds tighter than bitwise &, so the compiler builds std::cout << n first and then needs operator&(std::ostream&, int), which does not exist, so the program does not compile. The second option is tempting because 5 & 1 is obviously the intent, but grouping comes from the grammar rather than from intent, so the working form is std::cout << (n & 1).