Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 2 – Types, values and expressions

Integer overflow and signed/unsigned conversions

Why unsigned arithmetic wraps but signed overflow is undefined in C++, why -1 < v.size() is false, and how to catch overflow with UBSan and checks.

  • Intermediate
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Predict when integer arithmetic overflows and what then happens
  • Avoid signed/unsigned comparison bugs with size()
  • Detect overflow with UBSan and with checked arithmetic

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict when integer arithmetic overflows and what then happens
  • Avoid signed/unsigned comparison bugs with size()
  • Detect overflow with UBSan and with checked arithmetic

Before you start

On this page

Integers in C++ have a fixed number of bits, so a calculation can produce a result that does not fit. What happens then depends on the type. Unsigned arithmetic wraps round modulo 2ᴺ: 0 − 1 is the largest value, and the largest value + 1 is 0. Signed overflow is undefined behaviour: the compiler may assume it never happens, so code that relies on wrapping can be compiled into something else entirely. And when signed and unsigned values meet in one expression, the signed one is usually converted to unsigned, which is why -1 < v.size() is false. This lesson reproduces each case with Apple clang 21 and GCC 16, catches overflow at run time with UBSan and at compile time with constexpr, and gives the repairs: widen before you multiply, compare with std::cmp_less or std::ssize, and use checked or saturating arithmetic where a value can really grow too large.

Unsigned arithmetic wraps round

Two dials of 32-bit values: unsigned goes from its maximum on to 0, while int has a seam after INT_MAX that must not be crossed.32-bit values on a dialvalues grow clockwisemax02³¹wrapsunsignedmax + 1 is 0: definedINT_MAXINT_MIN0undefinedintINT_MAX + 1: undefined

Unsigned wraps round; int has a seam you must not cross

Text description of the diagram

Two circles show the values of a 32-bit integer type going round like a dial, growing clockwise.

  • Left, unsigned: 0 is at the top and 2³¹ at the bottom. The largest value, labelled max (2³² − 1), sits just before the top. A solid arrow crosses the top, marked "wraps": max + 1 is 0, and that is defined.
  • Right, int: 0 is at the top. At the bottom the largest int, INT_MAX, meets the smallest, INT_MIN. A dashed arrow across that seam is marked "undefined": INT_MAX + 1 is undefined behaviour, not INT_MIN.

An unsigned type of N bits holds 0 to 2ᴺ − 1, and the standard defines its arithmetic modulo 2ᴺ: every result is reduced into that range. Think of a dial. Going down from 0 lands on the largest value, and going up from the largest value lands on 0.

wrap.cpp C++ · wrap.cpp
#include <cstdint>
#include <limits>
#include <print>

int main() {
    unsigned stock = 0;
    --stock;                                    // below 0: wraps round to the largest value
    std::println("0 - 1 as unsigned:       {}", stock);

    unsigned top = std::numeric_limits<unsigned>::max();
    std::println("max + 1 as unsigned:     {}", top + 1u);

    std::uint8_t level = 250;
    level += 10;                                // 260 is stored modulo 256
    std::println("250 + 10 in std::uint8_t: {}", level);
}
Wrapping on both compilers Python · wrap_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra wrap.cpp -o wrap
./wrap
g++-16 -std=c++23 -Wall -Wextra wrap.cpp -o wrap
./wrap
""")

Output

$ clang++ -std=c++23 -Wall -Wextra wrap.cpp -o wrap
$ ./wrap
0 - 1 as unsigned:       4294967295
max + 1 as unsigned:     0
250 + 10 in std::uint8_t: 4
$ g++-16 -std=c++23 -Wall -Wextra wrap.cpp -o wrap
$ ./wrap
0 - 1 as unsigned:       4294967295
max + 1 as unsigned:     0
250 + 10 in std::uint8_t: 4

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wrap_session.py

Lines starting with $ are the commands typed; everything between them is the output of the command above. Both compilers print the same three lines, without a warning, because nothing here is an error in the language’s eyes. The last line has an extra step. Arithmetic on types narrower than int happens in int: level += 10 computes 250 + 10 = 260 as an int, then converts 260 back to std::uint8_t, keeping it modulo 256, which gives 4. Defined does not mean intended: a stock count that wraps to 4,294,967,295 is still a bug, only a predictable one.

Signed overflow is undefined behaviour

For signed types the standard makes no such promise. Since C++20, two’s complement is the only representation it allows, yet the note that P1236R1 added says it plainly: overflow for signed arithmetic is undefined behaviour. This program squares a side of 50,000, whose square, 2,500,000,000, is larger than the largest int, 2,147,483,647:

area.cpp C++ · area.cpp
#include <print>

int main() {
    int side = 50'000;
    long long wrong = side * side;         // int * int overflows before the result is widened
    long long right = 1LL * side * side;   // long long * int, then * int: no overflow
    std::println("wrong: {}", wrong);
    std::println("right: {}", right);
}
Signed overflow under UBSan Python · area_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=undefined area.cpp -o area
./area
g++-16 -std=c++23 -Wall -Wextra -fsanitize=undefined area.cpp -o area
./area
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=undefined area.cpp -o area
$ ./area
area.cpp:5:28: runtime error: signed integer overflow: 50000 * 50000 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior area.cpp:5:28 
wrong: -1794967296
right: 2500000000
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=undefined area.cpp -o area
$ ./area
area.cpp:5:28: runtime error: signed integer overflow: 50000 * 50000 cannot be represented in type 'int'
wrong: -1794967296
right: 2500000000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 area_session.py

-fsanitize=undefined builds the program with UndefinedBehaviorSanitizer (UBSan), which checks operations such as signed arithmetic while the program runs and reports the first one that goes wrong, with its file, line and column. Both compilers’ UBSan point at line 5. The value printed for wrong, -1,794,967,296, is what the processor’s wrapping instruction happened to produce, but nothing obliges the compiler to use that instruction or to keep the result.

The bug is the order of events. side * side multiplies two ints, so the product is an int and overflows; only then is it converted to long long, too late. 1LL * side * side makes the first operand a long long, so each multiplication is done in 64 bits. The order matters: side * side * 1LL still overflows, because the multiplications run from left to right and the first one is still int * int.

The compiler assumes overflow never happens

Undefined behaviour is not “the program crashes” or “the value wraps”. It is permission for the compiler to assume that the situation cannot arise, and optimisers use that permission. This function tries to detect overflow by checking whether x + 1 became smaller than x:

overflow_check.cpp C++ · overflow_check.cpp
#include <climits>
#include <print>

// Meant to detect overflow, but it relies on x + 1 wrapping round, which a signed int never has to do.
bool will_overflow(int x) {
    return x + 1 < x;
}

int main() {
    std::println("will_overflow(INT_MAX): {}", will_overflow(INT_MAX));
}
One function, three answers Python · overflow_check_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra overflow_check.cpp -o check
./check
clang++ -std=c++23 -Wall -Wextra -O2 overflow_check.cpp -o check
./check
g++-16 -std=c++23 -Wall -Wextra overflow_check.cpp -o check
./check
g++-16 -std=c++23 -Wall -Wextra -fsanitize=undefined overflow_check.cpp -o check
./check
""")

Output

$ clang++ -std=c++23 -Wall -Wextra overflow_check.cpp -o check
$ ./check
will_overflow(INT_MAX): true
$ clang++ -std=c++23 -Wall -Wextra -O2 overflow_check.cpp -o check
$ ./check
will_overflow(INT_MAX): false
$ g++-16 -std=c++23 -Wall -Wextra overflow_check.cpp -o check
$ ./check
will_overflow(INT_MAX): false
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=undefined overflow_check.cpp -o check
$ ./check
overflow_check.cpp:6:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
will_overflow(INT_MAX): true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 overflow_check_session.py

Apple clang without optimisation computes x + 1 with a wrapping instruction and says true. With -O2 it says false: since x + 1 cannot overflow in a valid program, x + 1 < x is always false, and the compiler replaced the whole test with false. GCC simplifies the comparison the same way even without optimisation. Under UBSan the addition really happens, so the report appears and the answer is true again. A check that depends on overflow cannot detect overflow. Compare against the limit before you add instead: x == INT_MAX, or in general x > INT_MAX - y for a positive y.

Overflow in a constant expression is an error

When a calculation runs at compile time, in a constexpr variable or a static_assert, the compiler is required to reject undefined behaviour instead of assuming it away:

constexpr_overflow.cpp C++ · constexpr_overflow.cpp
#include <climits>

constexpr int next(int x) {
    return x + 1;
}

constexpr int fine = next(41);          // evaluated while compiling: 42
constexpr int broken = next(INT_MAX);   // overflow while compiling: an error

int main() {
    return fine + broken;
}
Overflow found while compiling Python · constexpr_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -c constexpr_overflow.cpp
g++-16 -std=c++23 -Wall -Wextra -c constexpr_overflow.cpp
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c constexpr_overflow.cpp
constexpr_overflow.cpp:8:15: error: constexpr variable 'broken' must be initialized by a constant expression
    8 | constexpr int broken = next(INT_MAX);   // overflow while compiling: an error
      |               ^        ~~~~~~~~~~~~~
constexpr_overflow.cpp:4:14: note: value 2147483648 is outside the range of representable values of type 'int'
    4 |     return x + 1;
      |              ^
constexpr_overflow.cpp:8:24: note: in call to 'next(2147483647)'
    8 | constexpr int broken = next(INT_MAX);   // overflow while compiling: an error
      |                        ^~~~~~~~~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra -c constexpr_overflow.cpp
constexpr_overflow.cpp:8:28:   in 'constexpr' expansion of 'next(2147483647)'
    8 | constexpr int broken = next(INT_MAX);   // overflow while compiling: an error
      |                        ~~~~^~~~~~~~~
constexpr_overflow.cpp:4:14: error: overflow in constant expression [-fpermissive]
    4 |     return x + 1;
      |            ~~^~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 constexpr_session.py

fine compiles, and broken stops both compilers with the overflowing value spelled out. That makes constexpr evaluation a cheap way to test arithmetic helpers: a static_assert on an edge case, such as the largest input, runs every time the file compiles.

Mixing signed and unsigned

std::vector, std::string and the other standard containers report their size as std::size_t, an unsigned type. When an expression combines it with an int, the usual arithmetic conversions turn the int into the unsigned type, because unsigned wins when the unsigned type is at least as wide. A negative number does not survive that:

sign_compare.cpp C++ · sign_compare.cpp
#include <iterator>
#include <print>
#include <utility>
#include <vector>

int main() {
    std::vector<int> scores{70, 85, 92};
    int wanted = -1;                          // "no score": a common sentinel

    std::println("wanted < scores.size():            {}", wanted < scores.size());
    std::println("std::cmp_less(wanted, size):       {}", std::cmp_less(wanted, scores.size()));
    std::println("wanted < std::ssize(scores):       {}", wanted < std::ssize(scores));
    std::println("scores.size() - 4:                 {}", scores.size() - 4);
}
Comparing -1 with a size Python · sign_compare_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare
./sign_compare
g++-16 -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare
./sign_compare
""")

Output

$ clang++ -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare
sign_compare.cpp:10:66: warning: comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare]
   10 |     std::println("wanted < scores.size():            {}", wanted < scores.size());
      |                                                           ~~~~~~ ^ ~~~~~~~~~~~~~
1 warning generated.
$ ./sign_compare
wanted < scores.size():            false
std::cmp_less(wanted, size):       true
wanted < std::ssize(scores):       true
scores.size() - 4:                 18446744073709551615
$ g++-16 -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare
sign_compare.cpp: In function 'int main()':
sign_compare.cpp:10:66: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
   10 |     std::println("wanted < scores.size():            {}", wanted < scores.size());
      |                                                           ~~~~~~~^~~~~~~~~~~~~~~
$ ./sign_compare
wanted < scores.size():            false
std::cmp_less(wanted, size):       true
wanted < std::ssize(scores):       true
scores.size() - 4:                 18446744073709551615

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sign_compare_session.py

wanted < scores.size() converts -1 to a std::size_t, which on these 64-bit platforms is 18,446,744,073,709,551,615, the largest value, so the comparison is false. Both compilers warn about that line with -Wall -Wextra, and a build that treats warnings as errors catches it. The last line shows the same conversion in reverse: scores.size() - 4 is unsigned arithmetic, so 3 − 4 wraps round to the largest value instead of giving -1.

There are three good fixes, and rule ES.100 of the C++ Core Guidelines is behind all of them: don’t mix signed and unsigned arithmetic.

  • std::cmp_less(a, b) from <utility> (C++20) compares two integers by their mathematical values, whatever their types. Its family includes cmp_equal, cmp_greater and the others.
  • std::ssize(c) from <iterator> (C++20) returns a container’s size as a signed type, so the comparison is between two signed values.
  • Keep sizes in a signed type from the start when you do arithmetic with them, and convert once, deliberately.

Converting between integer types

Assigning a value to an integer type that cannot hold it is not overflow. Since C++20 the result is defined: the value is reduced modulo 2ᴺ for the target’s N bits, which keeps the low N bits of its two’s complement form.

conversions.cpp C++ · conversions.cpp
#include <cstdint>
#include <print>
#include <type_traits>

int main() {
    // Converting to an integer type that cannot hold the value keeps the low bits (defined since C++20).
    int from_big = static_cast<int>(3'000'000'000u);     // 3,000,000,000 - 2^32
    std::uint8_t from_300 = static_cast<std::uint8_t>(300);  // 300 - 256
    unsigned from_minus_one = static_cast<unsigned>(-1);  // 2^32 - 1
    std::println("{} {} {}", from_big, from_300, from_minus_one);

    // Arithmetic on small types happens in int: the operands are promoted first.
    unsigned char a = 200, b = 100;
    auto sum = a + b;
    static_assert(std::is_same_v<decltype(sum), int>);
    std::println("200 + 100 with unsigned char operands: {} (an int)", sum);
}
Conversions and promotions Python · conversions_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra conversions.cpp -o conversions
./conversions
g++-16 -std=c++23 -Wall -Wextra conversions.cpp -o conversions
./conversions
""")

Output

$ clang++ -std=c++23 -Wall -Wextra conversions.cpp -o conversions
$ ./conversions
-1294967296 44 4294967295
200 + 100 with unsigned char operands: 300 (an int)
$ g++-16 -std=c++23 -Wall -Wextra conversions.cpp -o conversions
$ ./conversions
-1294967296 44 4294967295
200 + 100 with unsigned char operands: 300 (an int)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 conversions_session.py

3,000,000,000 becomes -1,294,967,296 (3,000,000,000 − 2³²), 300 becomes 44 (300 − 256), and -1 becomes 4,294,967,295 (2³² − 1). Defined behaviour, but rarely what anyone wants, so make such conversions visible with static_cast, as the program does. The second half shows integral promotion: before arithmetic, operands narrower than int, such as unsigned char, short and bool, are promoted to int. That is why 200 + 100 gives 300 instead of wrapping in 8 bits, and why the type of the sum is int.

Programmer Calculator (Bitwise) Type 3000000000 and switch to 32-bit signed to see the same bits read as -1294967296.

Catching and preventing overflow

Three tools cover most needs.

UBSan, while you test. -fsanitize=undefined is available in both compilers, and Clang’s documentation lists macOS, Linux and Windows among its platforms. It reports signed overflow, and also division of INT_MIN by -1. Unsigned wrapping is not undefined, so it is not part of -fsanitize=undefined; Clang can check it with -fsanitize=unsigned-integer-overflow when a wrap would be a bug in your program.

Checked arithmetic, where an input can be too large. GCC and Clang provide __builtin_add_overflow, __builtin_sub_overflow and __builtin_mul_overflow, which compute the exact result, store it if it fits and return whether it overflowed. Wrapped in a function that returns std::optional, a caller cannot forget the check:

checked.cpp C++ · checked.cpp
#include <cstdint>
#include <limits>
#include <optional>
#include <print>
#include <utility>

// The product of a and b, or nothing when it does not fit in std::int64_t.
// __builtin_mul_overflow (GCC and Clang) computes the exact product and reports whether it fitted.
std::optional<std::int64_t> safe_mul(std::int64_t a, std::int64_t b) {
    std::int64_t product;
    if (__builtin_mul_overflow(a, b, &product)) return std::nullopt;
    return product;
}

int main() {
    constexpr std::int64_t lowest = std::numeric_limits<std::int64_t>::min();
    const std::pair<std::int64_t, std::int64_t> cases[] = {
        {3'000'000'000, 3},
        {3'037'000'499, 3'037'000'499},
        {3'037'000'500, 3'037'000'500},
        {lowest, 1},
        {lowest, -1},
    };
    for (auto [a, b] : cases) {
        if (auto product = safe_mul(a, b)) std::println("{} * {} = {}", a, b, *product);
        else std::println("{} * {} does not fit", a, b);
    }
}
A multiplication that reports overflow Python · checked_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra checked.cpp -o checked
./checked
g++-16 -std=c++23 -Wall -Wextra checked.cpp -o checked
./checked
""")

Output

$ clang++ -std=c++23 -Wall -Wextra checked.cpp -o checked
$ ./checked
3000000000 * 3 = 9000000000
3037000499 * 3037000499 = 9223372030926249001
3037000500 * 3037000500 does not fit
-9223372036854775808 * 1 = -9223372036854775808
-9223372036854775808 * -1 does not fit
$ g++-16 -std=c++23 -Wall -Wextra checked.cpp -o checked
$ ./checked
3000000000 * 3 = 9000000000
3037000499 * 3037000499 = 9223372030926249001
3037000500 * 3037000500 does not fit
-9223372036854775808 * 1 = -9223372036854775808
-9223372036854775808 * -1 does not fit

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 checked_session.py

The edge cases are the instructive ones. 3,037,000,499² fits in 64 bits and 3,037,000,500² does not, because the square root of the largest std::int64_t is about 3,037,000,499.98. And the smallest std::int64_t times -1 overflows, since the range runs to -2⁶³ but only up to 2⁶³ − 1. The same applies to -x, std::abs(x) and x / -1 when x is the smallest int, long or long long.

Big Number Calculator Check exact products such as 3037000500 × 3037000500 against the 64-bit limit.

Saturating arithmetic, where a clamp is the right answer. Sometimes the best result of an overflow is the nearest limit: a volume control, a pixel brightness, a counter that should stop at its maximum. C++26 adds these functions to <numeric>. They were adopted as std::add_sat and its relatives (P0543R3), and renamed std::saturating_add, std::saturating_sub, std::saturating_mul and std::saturating_div by P4052R0, which also raised the feature-test macro __cpp_lib_saturation_arithmetic from 202311 to 202603 so that code can tell the names apart. The two standard libraries here stand on either side of the rename:

saturating.cpp C++ · saturating.cpp
#include <climits>
#include <cstdint>
#include <limits>
#include <numeric>
#include <print>
#include <version>

// Adds and stops at the type's limits instead of overflowing. C++26 calls the standard
// function std::saturating_add; the first C++26 drafts called it std::add_sat.
template <class T>
constexpr T clamp_add(T a, T b) {
#if __cpp_lib_saturation_arithmetic >= 202603L
    return std::saturating_add(a, b);
#elif __cpp_lib_saturation_arithmetic >= 202311L
    return std::add_sat(a, b);
#else
    T sum;
    if (!__builtin_add_overflow(a, b, &sum)) return sum;
    return b > 0 ? std::numeric_limits<T>::max() : std::numeric_limits<T>::min();
#endif
}

int main() {
#ifdef __cpp_lib_saturation_arithmetic
    std::println("__cpp_lib_saturation_arithmetic = {}", __cpp_lib_saturation_arithmetic);
#else
    std::println("no saturation arithmetic in this library");
#endif
    std::println("int8: 100 + 100 = {}", clamp_add<std::int8_t>(100, 100));
    std::println("uint8: 250 + 10 = {}", clamp_add<std::uint8_t>(250, 10));
    std::println("int: INT_MAX + 1 = {}", clamp_add(INT_MAX, 1));
    std::println("int: INT_MIN + -1 = {}", clamp_add(INT_MIN, -1));
}
One file, two names, the same results Python · saturating_session.py
from session import run

run("""
clang++ -std=c++26 -Wall -Wextra saturating.cpp -o saturating
./saturating
g++-16 -std=c++26 -Wall -Wextra saturating.cpp -o saturating
./saturating
g++-16 -std=c++23 -Wall -Wextra saturating.cpp -o saturating
./saturating
""")

Output

$ clang++ -std=c++26 -Wall -Wextra saturating.cpp -o saturating
$ ./saturating
__cpp_lib_saturation_arithmetic = 202311
int8: 100 + 100 = 127
uint8: 250 + 10 = 255
int: INT_MAX + 1 = 2147483647
int: INT_MIN + -1 = -2147483648
$ g++-16 -std=c++26 -Wall -Wextra saturating.cpp -o saturating
$ ./saturating
__cpp_lib_saturation_arithmetic = 202603
int8: 100 + 100 = 127
uint8: 250 + 10 = 255
int: INT_MAX + 1 = 2147483647
int: INT_MIN + -1 = -2147483648
$ g++-16 -std=c++23 -Wall -Wextra saturating.cpp -o saturating
$ ./saturating
no saturation arithmetic in this library
int8: 100 + 100 = 127
uint8: 250 + 10 = 255
int: INT_MAX + 1 = 2147483647
int: INT_MIN + -1 = -2147483648

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 saturating_session.py

With -std=c++26, Apple clang’s libc++ reports 202311 and offers only std::add_sat, while GCC 16’s libstdc++ reports 202603 and offers only std::saturating_add. The #if tests choose whichever exists, and in C++23 mode, where neither does, the file falls back to __builtin_add_overflow and clamps by hand. All three builds print the same results: 100 + 100 in std::int8_t stops at 127, and 250 + 10 in std::uint8_t at 255.

Interview questions

Fresher · What happens when an unsigned int overflows, and when an int does?

Unsigned arithmetic is defined modulo 2ᴺ, so it wraps: the largest unsigned int plus 1 is 0, and 0 minus 1 is the largest value. Signed overflow, such as INT_MAX + 1, is undefined behaviour, even though C++20 requires two’s complement. The compiler may assume it never happens: it can fold a check such as x + 1 < x to false, and the program may print a wrapped value, a different value or something else. Converting an out-of-range value to a signed type is different: since C++20 that is defined and keeps the low bits. Catch overflow with -fsanitize=undefined while testing, and prevent it with wider types or checked arithmetic.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 unsigned is 32 bits here. What value does u hold afterwards?

    Read the code, then choose one answer.

    unsigned u = 0;
    u = u - 1;
    Show the answer to question 1

    Answer: 4294967295

    Unsigned arithmetic is defined modulo 2³², so 0 − 1 wraps round to 2³² − 1, the largest unsigned value. Only signed overflow is undefined.

  2. Question 2 of 6 What does the standard say about the result?

    Read the code, then choose one answer.

    int side = 50'000;
    long long area = side * side;
    Show the answer to question 2

    Answer: The behaviour is undefined, because side * side overflows int before it is widened

    Both operands are int, so the product is computed as an int, and 2,500,000,000 does not fit. The wrapped value that many builds print is not guaranteed. Write 1LL * side * side.

  3. Question 3 of 6 What does std::cmp_less(-1, v.size()) return for a vector v with two elements?

    Choose one answer.

    Show the answer to question 3

    Answer: true

    std::cmp_less compares the mathematical values, so -1 is less than 2. The plain comparison -1 < v.size() converts -1 to std::size_t, the largest value, and gives false.

  4. Question 4 of 6 Which of these are well-defined in C++23?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • static_cast<int>(3000000000u)
    • UINT_MAX + 1u
    • static_cast<std::uint8_t>(300)

    Conversions to an integer type keep the low bits (300 becomes 44, and since C++20 3,000,000,000 becomes -1,294,967,296), and unsigned arithmetic wraps. INT_MAX + 1 and -INT_MIN overflow int, which is undefined.

  5. Question 5 of 6 w and h are int. Which line computes their product without overflowing int?

    Choose one answer.

    Show the answer to question 5

    Answer: long long r = 1LL * w * h;

    Multiplication runs from left to right, so only when the first operand is a long long is every step done in 64 bits. In the other three, w * h is computed as an int first.

  6. Question 6 of 6 GCC turns return x + 1 < x;, with x an int, into return false; even without optimisation. Why is that allowed?

    Choose one answer.

    Show the answer to question 6

    Answer: Signed overflow is undefined, so the compiler may assume x + 1 never overflows

    In a program without undefined behaviour x + 1 is always greater than x, so the comparison is always false. Test against the limit before adding, for example x == INT_MAX.

Key takeaways

  • Unsigned arithmetic wraps modulo 2ᴺ: defined, but often still a bug.
  • Signed overflow is undefined behaviour. The compiler may assume it cannot happen, so a check like x + 1 < x can be compiled to false: compare against the limit before the operation instead.
  • Widen before the operation: 1LL * a * b, never long long r = a * b;.
  • In a constant expression, overflow is a compile error, which makes static_assert a cheap test of edge cases.
  • An int compared with a size() becomes unsigned, so -1 < v.size() is false. Use std::cmp_less, std::ssize or signed sizes.
  • Converting to a narrower integer type keeps the low bits (defined since C++20), and arithmetic on narrow types happens in int.
  • Test with -fsanitize=undefined; check with __builtin_mul_overflow; clamp with C++26 saturation arithmetic, std::saturating_add in GCC 16 and still std::add_sat in Apple clang 21’s libc++.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.