Lesson 3 of 8
An int holds about ±2 billion. A std::uint8_t holds 0 to 255. What happens when you try to
store 256 in the second one, or 3 billion in the first?
The answer differs, and the difference matters enormously.
Unsigned: wraps around, predictably
Unsigned types use modular arithmetic. Exceed the maximum and you wrap to zero; go below zero and you wrap to the maximum. This is fully defined by the standard — it is not a bug, it's the specified behaviour:
#include <cstdint>
#include <iostream>
int main() {
std::uint8_t small = 255;
small = small + 1;
std::cout << static_cast<int>(small) << "\n";
unsigned int zero = 0;
zero = zero - 1;
std::cout << zero << "\n";
return 0;
}
0
4294967295
255 + 1 wraps to 0. And 0 − 1 on an unsigned int doesn't give −1 — it gives 4,294,967,295, the
largest value a 32-bit unsigned can hold. There is no negative to go to.
That second one is the source of a genuinely common bug, coming up shortly.
Signed: undefined behaviour
Signed overflow is a completely different story:
int big = 2147483647; // the maximum int
big = big + 1; // undefined behaviour — anything may happen
This is not "wraps to negative." It is undefined behaviour (lesson 8.7): the standard imposes no requirement at all. In practice you might see a negative number, but the compiler is entitled to assume it never happens and optimise on that basis — which can delete an overflow check you wrote, or make a loop run forever.
The practical rule: signed overflow is always a bug, never a technique. If you need wraparound, use an unsigned type and say so.
Checking before you overflow
Don't compute and then check — by then it's too late. Compare against the limit first:
#include <iostream>
#include <limits>
int main() {
int value = 2000000000;
int addend = 200000000;
std::cout << std::numeric_limits<int>::max() << "\n";
if (value > std::numeric_limits<int>::max() - addend) {
std::cout << "Would overflow — refusing\n";
} else {
std::cout << value + addend << "\n";
}
return 0;
}
2147483647
Would overflow — refusing
<limits> gives you std::numeric_limits<T>::max() and ::min() for any numeric type. Rewriting
a + b > max as a > max - b does the same check without ever performing the overflowing
addition.
The size_t trap
Here's the one that actually catches people. Remember from lesson 3.2 that .size() returns
std::size_t, which is unsigned:
#include <iostream>
#include <vector>
int main() {
std::vector<int> empty;
std::cout << empty.size() << "\n";
std::cout << empty.size() - 1 << "\n";
return 0;
}
0
18446744073709551615
empty.size() is 0. empty.size() - 1 is not −1 — it's the largest possible size_t.
Now imagine the classic loop:
for (std::size_t i = 0; i < v.size() - 1; ++i) {
// compare v[i] with v[i + 1]
}
On a non-empty vector this is correct. On an empty one, v.size() - 1 is 18 quintillion, the
loop condition is true, and you immediately index off the end of an empty vector — reading memory
that isn't yours. That's a crash if you're lucky and silent corruption if you're not.
Two fixes, both fine:
if (v.size() > 1) {
for (std::size_t i = 0; i < v.size() - 1; ++i) { /* ... */ }
}
// or, avoid the subtraction entirely:
for (std::size_t i = 1; i < v.size(); ++i) { /* compare v[i-1] with v[i] */ }
Never subtract from an unsigned value unless you're certain it's large enough.
Don't mix signed and unsigned
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
int index = -1;
if (index < static_cast<int>(v.size())) {
std::cout << "negative index is less than size, as expected\n";
}
return 0;
}
negative index is less than size, as expected
Note the static_cast<int>. Without it, comparing index < v.size() compares a signed to an
unsigned — and C++ converts the signed one to unsigned first, turning −1 into 18 quintillion. The
comparison then comes out false, which is the opposite of what you meant.
-Wall -Wextra warns about this (comparison of integer expressions of different signedness).
It is one of the warnings most worth never ignoring.
Try it
Write a program that takes a starting std::uint8_t of 250 and adds 1 to it ten times,
printing the value each time. Predict the output before running it.
Checkpoint
- Unsigned overflow wraps around and is fully defined; signed overflow is undefined behaviour.
- Never rely on signed overflow — check against
std::numeric_limits<T>::max()before adding. .size()returns unsignedstd::size_t, sosize() - 1on an empty container is astronomically large, not −1.- Avoid subtracting from unsigned values; restructure the loop instead.
- Never compare signed with unsigned — the signed value converts, and negatives become huge.
Questions about this lesson
Log in to ask a question.