Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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 unsigned std::size_t, so size() - 1 on 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.