SECTION 1
Historical Context & Motivation
The concept of operator precedence in programming languages did not emerge in a vacuum — it was inherited from centuries of mathematical convention and refined through decades of compiler design. When Bjarne Stroustrup designed C++ as an extension of C in the early 1980s, he deliberately preserved the arithmetic operator semantics and precedence rules that Dennis Ritchie had established in C, which themselves drew heavily from Fortran and the algebraic notation standardized by mathematicians over the previous three centuries. Understanding this lineage illuminates why C++ evaluates 2 + 3 * 4 as 14 rather than 20, and why certain precedence quirks persist to this day.
1600s • Algebraic Notation Standardized
1957 • Fortran — First High-Level Compiler
1972 • C Language Created
1979–1985 • C++ Emerges
2011–2023 • Modern C++ Standards
The central question this lesson addresses is deceptively simple: given a C++ expression like a + b * c - d / e % f, how does the compiler determine the order of operations, what are the type semantics of each operator, and what subtle pitfalls — integer division truncation, overflow, implicit promotion — can silently corrupt your results? A rigorous understanding of these mechanics is foundational to writing correct and predictable C++ code.
SECTION 2
Core Principles & Definitions
C++ provides five fundamental arithmetic operators that operate on numeric types: addition, subtraction, multiplication, division, and the modulus (remainder) operator. These operators interact with two orthogonal but equally important concepts — precedence (which operator binds first) and associativity (which direction equal-precedence operators bind). Together, these rules completely determine how the compiler parses an expression into an abstract syntax tree (AST), thereby defining evaluation order.
The Five Arithmetic Operators
+ (addition), - (subtraction), * (multiplication), / (division), and % (modulus). The unary + and - are also arithmetic but have higher precedence as prefix operators.Precedence
* / %) have higher precedence than additive operators (+ -), mirroring standard algebra.Associativity
a - b - c parses as (a - b) - c.Integer vs. Floating-Point Division
/ operator truncates toward zero when both operands are integers (7 / 2 == 3), but performs true division when at least one operand is a floating-point type (7.0 / 2 == 3.5).Implicit Type Promotion
int → double) before the operation is performed.KEY TAKEAWAY
SECTION 3
Visualizing Precedence & the AST
The most illuminating way to understand how the compiler interprets an arithmetic expression is to examine the abstract syntax tree (AST) it constructs. Consider the expression 2 + 3 * 4 - 6 / 2. Because * and / have higher precedence than + and -, the tree places the multiplicative operations deeper (closer to the leaves), ensuring they are evaluated before the additive operations at the top. The following diagram shows this tree structure with evaluation results propagating upward.
2 + 3 * 4 − 6 / 2. Leaves are literal values; internal nodes are operators. Evaluation proceeds bottom-up: 3 * 4 → 12 and 6 / 2 → 3 resolve first, then 2 + 12 → 14, and finally 14 − 3 → 11.Notice how the multiplication and division nodes sit deeper in the tree than the addition and subtraction nodes. This depth relationship is the geometric manifestation of precedence: higher-precedence operators are pushed further down toward the leaves, ensuring they are evaluated first during a post-order traversal. If you were to add parentheses — say (2 + 3) * 4 — the addition node would be pushed below multiplication, fundamentally reshaping the tree and changing the result from 14 to 20.
SECTION 4
Operator Semantics & Type Rules
Each arithmetic operator in C++ carries specific type semantics governed by the standard's usual arithmetic conversions (§7.4 of the C++ standard). Before any binary arithmetic operation, the compiler applies integer promotion (types narrower than int — such as char, short, and bool — are promoted to int) and then, if operand types still differ, converts the narrower type to the wider one following a well-defined hierarchy.
INTEGER DIVISION TRUNCATION
For integer operands, C++ truncates the quotient toward zero. So 7 / 2 == 3 and -7 / 2 == -3 (not −4, which floor division would yield).
MODULUS IDENTITY
This identity always holds for integer a and non-zero integer b. Because division truncates toward zero, the sign of a % b matches the sign of a in C++11 and later.
TYPE PROMOTION HIERARCHY
When two operands have different types, the operand with the lower-ranked type is implicitly converted to the higher-ranked type before the operation executes. Mixing signed and unsigned integers within the same rank follows additional rules that can produce surprising results.
⚠ COMMON PITFALL
signed types is undefined behavior in C++. The expression INT_MAX + 1 does not wrap around to INT_MIN by specification — the compiler is free to assume it never happens and may optimize accordingly. For unsigned types, overflow wraps modulo 2ⁿ, which is well-defined.SECTION 5
Complete Arithmetic Precedence Table
The following table presents the precedence levels relevant to arithmetic in C++, extracted from the full 17-level precedence table defined by the standard. Remember that parenthesized sub-expressions are always evaluated first, effectively overriding any natural precedence. The table also includes the unary operators because the unary minus and plus are frequently confused with their binary counterparts despite having much higher precedence.
| Precedence Level | Operators | Description | Associativity |
|---|---|---|---|
| 2 (highest shown) | +x -x | Unary plus, unary minus | Right-to-left |
| 2 | ++x --x | Prefix increment / decrement | Right-to-left |
| 5 | * / % | Multiplication, division, modulus | Left-to-right |
| 6 | + - | Addition, subtraction | Left-to-right |
| 16 | = += -= *= /= %= | Assignment and compound assignment | Right-to-left |
Arithmetic-related precedence levels in C++ (lower number = higher precedence)
A critical observation is the gap between levels 2 and 5. Levels 3 and 4 are occupied by member access, dereferencing, and postfix operators, which are not arithmetic but frequently appear in arithmetic expressions (e.g., ptr->value * count). The compound assignment operators (+=, *=, etc.) sit at level 16, near the bottom, so the entire right-hand side expression is fully evaluated before the assignment takes place.
SECTION 6
Worked Example — Evaluating a Complex Expression
Let us trace the evaluation of a non-trivial expression that exercises precedence, associativity, integer division, and type promotion. Consider the following C++ code:
EXPRESSION
int result = 10 + 20 * 3 / 4 - 5 % 3 + 1.5;Step-by-Step Evaluation
- 1
Step 1 — Identify Operators and Precedence
The expression contains+(level 6),*(level 5),/(level 5),-(level 6),%(level 5),+(level 6), and=(level 16). Level-5 (multiplicative) operators bind first, then level 6 (additive), then assignment. - 2
Step 2 — Evaluate Multiplicative Operators (Left-to-Right)
Among the level-5 operators, left-to-right associativity means we process20 * 3first. Since both areint, the result is60. Next,60 / 4yields15(integer division, but exact in this case). Finally,5 % 3yields2.Expression becomes:10 + 15 - 2 + 1.5 - 3
Step 3 — Evaluate Additive Operators (Left-to-Right)
All remaining binary operators are at level 6. Left-to-right:10 + 15→25; then25 - 2→23; then23 + 1.5. Here, the1.5is adoubleliteral, triggering implicit promotion of23(int) to23.0(double). The result is24.5as adouble.Sub-expression result:24.5(double) - 4
Step 4 — Assignment with Narrowing
The target variableresultis declared asint, so thedoublevalue24.5is truncated to24. This is an implicit narrowing conversion — most compilers will issue a warning at higher warning levels, and brace initialization (int result{24.5}) would produce a compile-time error.Final result:result == 24
KEY TAKEAWAY
SECTION 7
Common Pitfalls & Best Practices
While C++ arithmetic operators appear straightforward, a number of subtle behaviors catch even experienced developers. The table below catalogues the most common pitfalls alongside recommended practices. Many of these arise from the interaction between precedence rules and the type system, making them particularly insidious in mixed-type expressions.
| Pitfall | Example | Best Practice |
|---|---|---|
| Integer division truncation | int avg = sum / count; loses fractional part | Cast one operand: static_cast<double>(sum) / count |
| Signed integer overflow (UB) | int x = INT_MAX + 1; is undefined | Check before operation, or use unsigned/wider types |
| Modulus with negative operands | -7 % 3 == -1 may surprise | Remember: result sign matches dividend in C++11+ |
| Precedence with bit operators | x & 0xFF + 1 parses as x & (0xFF + 1) | Always parenthesize bitwise ops in mixed expressions |
| Implicit signed/unsigned mixing | -1 < 1u evaluates to false | Enable -Wsign-compare and match signedness |
| Division by zero | x / 0 is UB for integers, ±inf for floats | Guard with a conditional check before dividing |
Common arithmetic pitfalls in C++ and recommended mitigations
KEY TAKEAWAY
SECTION 8
Connection to Operator Overloading & Advanced Topics
The arithmetic operators covered in this lesson operate on built-in types, but C++ extends these semantics to user-defined types through operator overloading. When you define operator+ for a class, the precedence and associativity of + remain fixed — you cannot change them. This means the precedence table you learned here governs all uses of these operators, whether on int, double, std::complex, or your own matrix class.
| Feature | Built-in Arithmetic | Overloaded Operators |
|---|---|---|
| Precedence | Fixed by the standard | Same — cannot be changed |
| Associativity | Fixed (left-to-right for binary arithmetic) | Same — cannot be changed |
| Operand types | Built-in numeric types only | Any user-defined class or struct |
| Type promotion | Implicit (usual arithmetic conversions) | Explicit — you define conversion constructors |
| Compile-time evaluation | constexpr by default | constexpr if marked and body is constexpr-compatible |
| Overflow behavior | UB for signed, wrapping for unsigned | Defined by your implementation (can throw, saturate, etc.) |
Built-in vs. overloaded arithmetic operators in C++
Looking further ahead, the constexpr and consteval features of modern C++ allow arithmetic expressions to be evaluated entirely at compile time, with the compiler performing the same precedence-driven evaluation you would do by hand. In template metaprogramming, arithmetic on std::integral_constant values operates at the type level, yet the same operator semantics apply. Understanding the fundamentals covered here is therefore prerequisite to advanced techniques like expression templates, which libraries such as Eigen and Blaze use to defer and optimize chains of arithmetic operations on matrices and vectors.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
3 + 4 * 2 evaluates to 11 rather than 14. Which specific C++ rule determines this behavior, and what role does associativity play (or not play) here?Show answer
* has higher precedence (level 5) than the addition operator + (level 6). Therefore, 4 * 2 is evaluated first, yielding 8, and then 3 + 8 yields 11. Associativity is irrelevant here because the two operators are at different precedence levels; associativity only breaks ties among operators at the same level.Question 2: BASIC CALCULATION
17 / 5 + 17 % 5? Show your work for each sub-expression.Show answer
/ and % are at precedence level 5, so they are evaluated left-to-right. First, 17 / 5 performs integer division, truncating toward zero: result is 3. Next, 17 % 5 yields the remainder 2 (since 5 × 3 + 2 = 17). Finally, 3 + 2 = 5. The result type is int. Note that this illustrates the modulus identity: (a/b)*b + (a%b) == a, so 17/5 + 17%5 always reconstructs a/b + a%b — which equals 5 here.Question 3: INTERMEDIATE
int a = 7;
double b = 2.0;
int c = a / 3 + b * 3;
std::cout << c;Show answer
a / 3 is 7 / 3 = 2 (integer division, both operands are int). Then b * 3: the 3 is promoted to double, yielding 6.0 (type promotion occurs here). Now 2 + 6.0: the 2 is promoted to 2.0, yielding 8.0 (second type promotion). Assignment to int c truncates 8.0 to 8 (narrowing conversion). Output: 8.Question 4: APPLIED
int array. Your colleague writes: int avg = (readings[0] + readings[1] + readings[2]) / 3; Given readings {10, 11, 12}, what does avg equal? What if readings are {10, 11, 13}? Propose a fix that preserves floating-point precision.Show answer
double avg = static_cast<double>(readings[0] + readings[1] + readings[2]) / 3; The cast converts the sum to double before division, triggering type promotion of 3 to 3.0, yielding 11.333.... Note that the cast must be on the sum, not the individual element, to avoid precision issues from summing doubles unnecessarily.Question 5: CRITICAL THINKING
unsigned int x = 5; int y = -3; auto z = x + y; What is the value and type of z? Explain the implicit conversion that occurs, why the result is mathematically correct despite the unsigned conversion of y, and under what conditions this pattern would produce a logically incorrect result.Show answer
unsigned int and int are mixed at the same rank, the signed operand is converted to unsigned (per §7.4 of the standard). So y = -3 becomes UINT_MAX - 2 (a very large number, e.g., 4294967293 on 32-bit). Adding 5 + 4294967293 overflows (wrapping modulo 2³²), yielding 2. The type of z is unsigned int. The result (2) is mathematically correct here because 5 + (−3) = 2, and the modular arithmetic happens to produce the right answer. However, if y were −10, the result would be UINT_MAX - 4 — a huge positive number, not −5. Any time the true mathematical result is negative, the unsigned conversion yields a logically incorrect (but well-defined) positive value.SUMMARY
Lesson Summary
Summary
C++ provides five arithmetic operators — +, -, *, /, and % — governed by a strict precedence hierarchy inherited from mathematical convention and the C language. Multiplicative operators (* / %) bind more tightly than additive operators (+ -), and all binary arithmetic operators are left-to-right associative. Parentheses override all precedence rules and should be used liberally for clarity.
Beyond grouping, the type system profoundly affects results: integer division truncates toward zero, the modulus identity (a/b)*b + a%b == a always holds, and implicit type promotion silently converts narrower types to wider ones. Signed integer overflow is undefined behavior, while unsigned overflow wraps modulo 2ⁿ. These same precedence and associativity rules extend unchanged to overloaded operators on user-defined types, making mastery of these fundamentals essential for all subsequent C++ development.