Chapter 05 of 24 | C++

Expressions and operators

An expression combines values and operators to produce a result. Arithmetic operators include +, -, *, /, and %. Integer division discards the fractional part: 7 / 2 is 3, while 7.0 / 2 is 3.5.

Precedence and grouping

Multiplication and division bind more tightly than addition and subtraction, but parentheses are clearer than relying on memory. Write (a + b) * scale when that is the intended calculation. Precedence controls grouping, not necessarily the order in which every subexpression is evaluated.

int a = 7 / 2;            // 3
double b = 7.0 / 2;       // 3.5
int remainder = 7 % 2;    // 1

Division by zero is invalid. Integer overflow for signed types is also not a wraparound mechanism you can rely on; ensure arithmetic stays within the representable range.

Comparisons and logic

Comparisons such as ==, !=, <, and >= produce a bool. Use && for logical AND, || for OR, and ! for NOT. These operators short-circuit: the right operand is skipped when the left operand already determines the result.

bool in_range = value >= low && value <= high;
bool safe_to_divide = divisor != 0 && numerator / divisor > 1;

The second example is safe because the division is evaluated only if divisor != 0 is true. Do not confuse logical && with bitwise &, or equality == with assignment =.

Assignment and updates

count += 1 updates a variable using its previous value. The forms ++count and count++ both increase it, but differ in the value of the expression: prefix yields the updated value; postfix yields the old value. Prefer the form that makes intent clearest, and avoid clever expressions that both modify and read the same object.

Integer division can change an algorithm

When both operands of / are integers, C++ performs integer division and discards the fractional part toward zero. It does not round to the nearest integer. This can create bugs in averages and percentages:

int points{7};
int questions{2};
double wrong_average = points / questions;  // division happens as int: 3
double average = static_cast<double>(points) / questions; // 3.5

Assigning the result to a double does not change the type of the operation that has already happened. At least one operand must be floating point before division if a fractional answer is needed. When converting, make the intent clear and verify the input values are in range.

Comparisons have traps

Equality uses two equals signs: score == target. A single = assigns a value. The compiler can warn about some suspicious assignments in conditions, but expressing the comparison correctly is your responsibility. Chained mathematical notation such as 0 < x < 10 does not mean “x is between zero and ten” in C++; it compares 0 < x first and then compares that Boolean result with 10. Write x > 0 && x < 10.

Floating-point equality is also often the wrong test for calculated measurements. If exact equality matters, use an exact representation. If a tolerance is appropriate, compare the magnitude of the difference with a tolerance chosen for the scale and accuracy requirements of the calculation.

Order, short-circuiting, and side effects

Short-circuit evaluation is useful for safely checking a condition before using a value. In pointer != nullptr && *pointer > 0, the dereference only happens if the pointer check succeeds. But short-circuiting should not be used to hide important work inside the right operand. Keep state-changing operations in their own statements so the program's order is easy to understand.

Parentheses are a low-cost way to communicate how a complicated condition is grouped. When a condition becomes difficult to read, name its parts with Boolean variables or split it into helper functions. Clear intent is more valuable than saving a few characters.

Remainder and negative numbers

The remainder operator % is defined for integer operands, not floating-point operands. It is often useful for divisibility checks such as number % 2 == 0. With negative values, the remainder follows the sign and division rules of C++; do not assume it always produces a nonnegative mathematical modulus. If a domain requires a normalized nonnegative result, write and test that normalization explicitly.

Practice

Key idea: know the type of each operand, group calculations clearly, and keep side effects separate from complex expressions.

Sequencing is deeper than precedence

Operator precedence tells the parser how an expression is grouped; it does not, by itself, define the order in which operands are evaluated. C++ has specific sequencing rules for particular operators and expressions. If two unsequenced operations modify the same scalar object, the program may have undefined behavior. Expressions such as i++ + i++ are therefore not portable puzzles with one secret answer; they are code to avoid.

Use separate statements when order matters. This is easier to review, avoids depending on subtle sequencing rules, and gives a debugger a clear place to inspect intermediate values. In generic code, sequencing can become even more subtle when overloaded operators execute arbitrary user functions, so favor explicit named operations over clever operator chains.

Overflow-safe arithmetic and checked domains

Signed integer overflow is undefined behavior. If a number can approach a type's limit, check before performing addition or multiplication. For nonnegative values a and b, addition is safe only when a <= max - b. Similar checks are needed for multiplication, taking care not to overflow while computing the check itself. The right rule depends on the permitted sign and range of the inputs.

Unsigned arithmetic is defined modulo a power of two, but wraparound can still violate a program's domain rules. A wrapped allocation size or counter may become unexpectedly small. Use checked arithmetic or a suitable library abstraction when the value's mathematical meaning must be preserved.

Operator overloading and algebraic expectations

User-defined types can overload operators, but a familiar symbol creates expectations. If a + b is overloaded, readers expect a value-producing operation that does not unexpectedly mutate global state. Comparison operators should be consistent with the ordering or equality required by algorithms and containers. Prefer named functions when the operation is not naturally analogous to the built-in operator.

Mathematical identities are not always valid under machine arithmetic. Floating-point addition is not associative, integer overflow changes the valid domain, and custom overloaded operators may implement domain-specific behavior. Before applying an algebraic transformation for speed or clarity, check that it preserves the program's actual type semantics.

Advanced exercise

  1. Use compiler warnings and sanitizers to investigate arithmetic near the boundaries of a signed type.
  2. Rewrite a compact expression with multiple side effects as sequenced statements and explain why it is easier to reason about.
  3. Implement a checked-add operation that reports failure rather than overflowing, and test minimum, maximum, and mixed-sign inputs.
0% read