C++ / OPERATORS AND EXPRESSIONS
Arithmetic operators and integer division traps
Predict and control what C++ arithmetic operators return, so int/int truncation, negative remainders and unsigned wraparound stop producing wrong numbers.
What you will learn
- Predict when / truncates: two integral operands discard the fraction.
- Divide with static_cast<double> on an operand, never on the result.
- Derive -7 / 2 == -3 and -7 % 2 == -1 from truncation toward zero.
- Spot unsigned subtraction wrapping to a huge value instead of going negative.
Understanding Arithmetic operators and integer division traps
C++ gives you +, -, *, / and %, but / is really two different operations wearing one symbol. Which one you get is decided by the types of the operands, not by what you intend to do with the answer: if both sides are integral types the compiler emits integer division, which computes the quotient and then throws away everything after the decimal point. So 7 / 2 is 3 and 1 / 2 is 0, and storing that in a double afterwards cannot bring the lost half back.
The rule that picks the type is the set of usual arithmetic conversions, and it is applied to each binary operator separately. If either operand is a floating-point type the other is converted to it and the work happens in floating point; otherwise both operands are promoted to at least int and the result is an integer type. That is why 100.0 * passed / total works while passed / total * 100.0 does not: in the second version the / already has two int operands and has truncated before the double appears. Read an expression as a tree evaluated bottom-up, where every node chooses its own result type from its children.
% is the companion of integer /: for integral a and b with b != 0, the identity (a / b) * b + a % b == a always holds. Since C++11, / truncates toward zero, and that identity then forces a % b to carry the sign of a, so -7 / 2 is -3 and -7 % 2 is -1, not the -4 and 1 that a mathematician's floor division gives. Two corners of arithmetic have no value at all: integer division or remainder by zero is undefined behavior rather than an exception or inf, and signed overflow is undefined too, while unsigned arithmetic silently wraps modulo 2^N.
<iostream>
int main() {
int total = 7;
int count = 2;
std::cout << "int / int : " << total / count << '\n';
std::cout << "int % int : " << total % count << '\n';
std::cout << "cast operand : " << static_cast<double>(total) / count << '\n';
std::cout << "cast result : " << static_cast<double>(total / count) << '\n';
std::cout << "-7 / 2 = " << -7 / 2 << " -7 % 2 = " << -7 % 2 << '\n';
std::cout << " 7 / -2 = " << 7 / -2 << " 7 % -2 = " << 7 % -2 << '\n';
return 0;
}
The type of a division expression is fixed by its operand types, so int / int discards the fraction before any conversion, cast or assignment can rescue it.
Worked examples
Percentages and rounding up
Shows how operand order and one double literal change the result of the same arithmetic.
<iostream>
int main() {
int passed = 17;
int total = 40;
std::cout << "wrong: " << passed / total * 100 << "%\n";
std::cout << "right: " << passed * 100 / total << "%\n";
std::cout << "exact: " << 100.0 * passed / total << "%\n";
int items = 17;
int perPage = 5;
std::cout << "pages: " << (items + perPage - 1) / perPage << '\n';
return 0;
}
Example explained
Line 1passed / total is int / int, so it evaluates to 0 and the later * 100 just multiplies zero.
Line 2passed * 100 / total multiplies first so 1700 survives, but the divide still truncates 42.5 down to 42.
Line 3100.0 * passed makes that multiplication double, and the double type propagates into the following division.
Line 4(items + perPage - 1) / perPage is ceiling division for non-negative values: it only bumps the quotient when a remainder exists.
Remainders with negatives and with doubles
Demonstrates that % follows the sign of the left operand and does not accept floating-point operands.
<cmath>
<iostream>
int main() {
std::cout << "7.5 / 2.0 = " << 7.5 / 2.0 << '\n';
std::cout << "fmod(7.5, 2.0) = " << std::fmod(7.5, 2.0) << '\n';
int n = 5;
for (int i = -2; i <= 2; ++i) {
std::cout << i << " % " << n << " = " << i % n
<< " wrapped = " << ((i % n) + n) % n << '\n';
}
return 0;
}
Example explained
Line 17.5 / 2.0 has floating-point operands, so nothing is truncated and the fraction .75 is kept.
Line 2% rejects double operands at compile time; std::fmod from <cmath> is the floating-point remainder.
Line 3i % n keeps the sign of the left operand, so -2 % 5 is -2, which as an array index reads before the start of the array.
Line 4((i % n) + n) % n adds one modulus and reduces again, forcing the result into 0..n-1 for any i greater than -n.
Unsigned subtraction wraps instead of going negative
Shows that a subtraction between unsigned values produces a huge positive number that then flows into later arithmetic.
<cstdint>
<iostream>
int main() {
std::uint32_t stock = 3;
std::uint32_t ordered = 5;
std::cout << "difference : " << stock - ordered << '\n';
std::cout << "halved : " << (stock - ordered) / 2 << '\n';
std::cout << "signed : "
<< static_cast<std::int32_t>(stock) - static_cast<std::int32_t>(ordered)
<< '\n';
return 0;
}
Example explained
Line 1Both operands stay unsigned because int cannot represent every std::uint32_t value, so no promotion to a signed type happens (assuming the usual 32-bit int).
Line 23 - 5 is therefore computed modulo 2^32 and yields 4294967294, with no error at compile time or run time.
Line 3Dividing that wrapped value by 2 gives 2147483647, a plausible-looking number, which is why the bug survives review.
Line 4Converting each operand to a signed type before subtracting is what actually produces -2.
Important notes
Integer division or remainder by zero is undefined behavior, not an exception; only floating-point division by zero is defined, giving inf or nan under IEEE 754. std::numeric_limits<int>::min() / -1 overflows and is undefined too.
Truncation toward zero for / with negative operands is guaranteed only from C++11 onward; C++03 left the rounding direction implementation-defined, so old code sometimes contains workarounds you no longer need.
Common mistakes
Casting the result instead of an operand: static_cast<double>(a / b) divides in int first, so 7 / 2 becomes 3.0 rather than 3.5 — the cast only widens a value that has already lost its fraction.
Writing sum / count * 100 for a percentage: whenever sum is smaller than count the division yields 0, so the printed figure is 0% no matter how large the multiplier.
Assuming % never returns a negative number: -1 % 8 is -1, and using it directly as a container index is an out-of-bounds access, not a wrap to the last element.
Try it yourself
Change, predict, then run
Start from int seconds = 3725; and print it as "1h 2m 5s" using only / and % on integers, then change the value to -3725 and explain in a comment why every component comes out negative.
Open the C++ workspaceCheck your understanding
With int sum = 5; int count = 2;, the statement double avg = sum / count; leaves avg printing as 2. Why?
- std::cout rounds doubles toward zero when it prints them.
- Both operands are int, so integer division runs first and 2.5 is truncated to 2 before any double exists.
- double cannot represent 2.5 exactly, so it stores the nearest whole number.
- Initializing a double from an arithmetic expression rounds the result to an integer.
Show answer
The operand types alone select integer division, which produces 2; the initialization then merely widens that 2 to 2.0, so the fraction was gone before the double was involved. The representability option is tempting but wrong: 2.5 is exactly representable in binary floating point, and printing a double does not truncate — std::cout would happily show 2.5 if the value were 2.5.