Skip to content
VLSI Mentor

C++ Part I · Module 3 · Types, Variables, and Initialization

Integer Types, Signedness and Wraparound

Every integer type in C++ comes in two forms, and the choice between them is not a formality. One form can represent negative numbers and the other cannot, which changes the range you get from the same amount of storage. That part is easy. The part that causes real bugs is what happens when a calculation goes outside the range a type can hold, because C++ answers that question in two completely different ways depending on which form you picked. For one of them the result is fully specified and portable. For the other the standard says nothing at all, which means the program has no defined meaning — and no warning appears at the strongest warning level this curriculum uses. This chapter separates the two cases precisely, shows each one measured rather than asserted, and then works through the comparison that trips up careful engineers: a perfectly ordinary test between a signed and an unsigned value that quietly returns the opposite of what everyone expects.

Foundation18 min readC++Part IFundamentalsIntegers

Part I · Module 3 · Chapter 3.3 · Integer Types, Signedness and Wraparound

1. Two Forms of Every Integer Type

Each integer type has a signed and an unsigned form:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int           cycle_delta   = -5;    // signed: can be negative
unsigned int  packet_count  = 5;     // unsigned: cannot be negative

Plain int, short, long and long long are signed. Writing unsigned in front gives the unsigned form. (char is a special case and Chapter 3.6 covers it.)

Both forms use the same amount of storage — Chapter 3.2 §3. What differs is how the bits are interpreted, which is Chapter 3.1 §5 in action: same storage, different type, different meaning.

2. What Signedness Buys and Costs

With a fixed number of bits you have a fixed number of distinct patterns. A 32-bit type has 2³² of them, and that total does not change. Signedness decides how you spend them.

Unsigned spends them all on non-negative numbers: 0 up to 2³² − 1.

Signed spends about half on negatives: roughly −2³¹ up to 2³¹ − 1.

Measured on the machine used for this chapter, where int is 32 bits:

Azvya Education Pvt. Ltd.VLSI Mentor
ranges.cpp — complete program
#include <climits>
#include <iostream>
#include <limits>

int main()
{
    std::cout << "int  min = " << std::numeric_limits<int>::min() << '\n';
    std::cout << "int  max = " << std::numeric_limits<int>::max() << '\n';
    std::cout << "unsigned int max = " << std::numeric_limits<unsigned int>::max() << '\n';
    return 0;
}

Example output on this platform:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int  min = -2147483648
int  max = 2147483647
unsigned int max = 4294967295

Note the asymmetry in the signed range: one more negative value than positive. That falls out of the usual representation and is not something to memorise.

std::numeric_limits from <limits> is the portable way to ask these questions. Do not look these numbers up and type them in — ask, the way Chapter 3.2 §6 said to ask for sizes.

So the trade is simple: unsigned doubles your positive range; signed lets you go below zero. That is the easy half of the chapter.

3. The Question That Matters

What happens when a calculation produces a result the type cannot hold?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    unsigned int count = 4294967295;   // the largest value
    count = count + 1;                 // now what?

C++ gives two different answers to this question, depending on signedness, and the difference is not a detail. One case is fully specified and portable; the other has no specified behaviour at all.

Take them separately, because merging them is the mistake this chapter exists to prevent.

4. Unsigned: Wraparound, and It Is Guaranteed

For an unsigned type, the rule is exact:

Unsigned arithmetic is performed modulo 2ᴺ, where N is the number of bits in the type. A result that does not fit wraps around, and this behaviour is guaranteed by the standard.

"Modulo 2ᴺ" means: compute the mathematical answer, then take the remainder after dividing by 2ᴺ. The value never escapes the range; it cycles.

Azvya Education Pvt. Ltd.VLSI Mentor
wrap_unsigned.cpp — complete program. This behaviour is defined.
#include <iostream>
#include <limits>

int main()
{
    unsigned int counter = std::numeric_limits<unsigned int>::max();
    std::cout << "before  = " << counter << '\n';
    counter = counter + 1;                      // defined: wraps modulo 2^32
    std::cout << "after   = " << counter << '\n';

    unsigned int zero = 0;
    std::cout << "0 - 1   = " << zero - 1 << '\n';   // defined: wraps to the max
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before  = 4294967295
after   = 0
0 - 1   = 4294967295

Both results are guaranteed. The largest value plus one is 0. Zero minus one is the largest value.

That second line deserves attention: an unsigned value can never be negative, so subtracting past zero does not give you a negative number — it gives you a very large positive one. This is the single most common way unsigned types produce a wrong answer, and §7 shows it causing a real bug.

The behaviour holds at any optimisation level, because it is a guarantee rather than an accident:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -O2 wrap_unsigned.cpp -o wrap && ./wrap
after   = 0
0 - 1   = 4294967295

Identical. Keep that in mind for the next section, where the same test does something very different.

The modulus follows the width of the type, so a narrower type wraps sooner:

Azvya Education Pvt. Ltd.VLSI Mentor
wrap8.cpp — modulo 2^8
#include <cstdint>
#include <iostream>

int main()
{
    std::uint8_t packet_count = 255;
    packet_count = static_cast<std::uint8_t>(packet_count + 1);
    std::cout << "255 + 1 in a uint8_t = " << static_cast<unsigned>(packet_count) << '\n';
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
255 + 1 in a uint8_t = 0

Chapter 3.4 introduces std::uint8_t properly. The static_cast appears because arithmetic on small types produces a wider result — that is integer promotion, and Module 4 owns it. The casts to unsigned in the output exist because of a printing quirk Chapter 3.4 §6 explains.

5. Signed: Undefined Behaviour, and It Is Not Wraparound

Now the same experiment with a signed type. This is the most important paragraph in the chapter:

Signed integer overflow is undefined behaviour. It is not wraparound. The C++ standard does not say that a signed value that overflows becomes negative, or wraps, or does anything at all.

Many people believe signed overflow wraps the way unsigned does. It is a reasonable guess — the hardware often does wrap — and it is wrong about C++. Chapter 1.3 §5 established what "undefined behaviour" means: the standard places no requirements on the program, so there is no correct answer to predict.

Here is a program that overflows a signed integer:

Azvya Education Pvt. Ltd.VLSI Mentor
overflow_signed.cpp — INCORRECT. Signed overflow is undefined behaviour.
#include <climits>
#include <iostream>

int main()
{
    int cycle_count = INT_MAX;
    std::cout << "before = " << cycle_count << '\n';
    cycle_count = cycle_count + 1;        // undefined behaviour
    std::cout << "after  = " << cycle_count << '\n';
    return 0;
}

This chapter will not tell you what that program printed. It did print something on the machine used here, and quoting it would teach you a falsehood: that there is a result to expect. There is not. The program has no defined meaning, and a number produced by one run of one build is not evidence of a rule.

What is worth showing is how you find out the program is broken.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic overflow_signed.cpp -o ov
$                                            ← no diagnostic at all

Nothing. At the strongest warning level this curriculum uses, the compiler said nothing about it.

Now instrument the program to check for undefined behaviour while it runs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -fsanitize=undefined overflow_signed.cpp -o ov_san && ./ov_san
overflow_signed.cpp:9:31: runtime error: signed integer overflow:
                          2147483647 + 1 cannot be represented in type 'int'

A file, a line, a column, and the exact arithmetic that has no defined result. That is the evidence. The tool is the UndefinedBehaviorSanitizer, and Module 25.6 teaches using it as a routine.

6. Choosing Signedness

A short decision procedure, in order:

Can the quantity be negative? A temperature delta, a timing margin, an offset that can go backwards. If yes, it is signed. This is not a preference.

Is it a counter or a size that genuinely cannot be negative? Then unsigned is reasonable — and read §7 before deciding, because "cannot be negative" turns out to be a weaker argument than it looks.

Is it a hardware value where the width is part of the specification? Then the width matters more than the signedness, and Chapter 3.4 is the chapter.

Do you need wraparound? A free-running counter that should cycle — a sequence number, a ring-buffer index — is the one case where you want the wrap. Use unsigned and say so in a comment, because a reader cannot tell deliberate wrapping from an unnoticed bug.

Otherwise, prefer signed for ordinary arithmetic. This surprises people, and §7 is the reason.

7. "It Cannot Be Negative" Is Not Enough

Here is the argument that unsigned is the obvious choice for a length: a length cannot be negative, and unsigned cannot represent negatives, so the type matches the quantity.

It is a good argument and it is incomplete, because types do not exist in isolation — they meet other types in expressions. Watch:

Azvya Education Pvt. Ltd.VLSI Mentor
mixed.cpp — compiles; the result is not what anyone intended
#include <iostream>

int main()
{
    int          offset = -1;
    unsigned int length = 4;

    if (offset < length) {
        std::cout << "offset < length\n";
    } else {
        std::cout << "offset >= length  <-- surprising\n";
    }
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
offset >= length  <-- surprising

−1 is not less than 4.

Here is why, and the mechanism is worth knowing precisely. When a signed and an unsigned value of the same rank meet in a comparison, the signed one is converted to unsigned before the comparison happens. -1 converted to unsigned int is 4294967295 — the same modulo-2ᴺ rule from §4. The comparison then asks whether 4294967295 is less than 4, which it is not.

Every step is well defined. The conversion is specified, the comparison is specified, and the answer is specified. The program is not undefined — it is simply wrong, which in a verification context is worse, because nothing is going to flag it at run time.

Something does flag it at compile time, though:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mixed.cpp:7:16: warning: comparison of integers of different signs:
                'int' and 'unsigned int' [-Wsign-compare]

This is -Wall earning its place, exactly as Chapter 2.3 §5 argued.

The honest conclusion:

Unsigned does not mean "a number that cannot be negative". It means "a number whose arithmetic is modulo 2ᴺ, and which turns any negative value it meets into a very large positive one."

That is a stronger and more useful description, and it explains why experienced C++ programmers often reach for signed types in ordinary arithmetic even when the quantity cannot logically be negative. A signed type that accidentally goes negative gives you a negative number, which looks wrong immediately. An unsigned type that accidentally goes negative gives you four billion, which can sail through a range check.

Module 4.4 covers comparison operators and Module 4.10 covers the conversion rules in full. The habit you need now is simpler than the rules: do not mix signed and unsigned in one expression, and turn on the warning that tells you when you have.

8. Mistakes Worth Making Once

Believing signed overflow wraps. It is undefined behaviour. The hardware may wrap; the language does not promise it, and the optimiser has already assumed it cannot happen.

Expecting a warning for signed overflow. There was none at -Wall -Wextra -Wpedantic. A sanitizer found it at run time; the compiler did not find it at compile time.

Subtracting past zero in an unsigned type. 0u - 1 is 4294967295, not −1. This is defined, correct, and almost never what was wanted.

Writing a countdown loop with an unsigned counter. A loop that runs while the counter is >= 0 never ends, because an unsigned value is always >= 0. The compiler may warn; the logic error is yours.

Mixing signed and unsigned in a comparison. -1 < 4u is false. Everything about it is defined and the answer is still wrong. Keep -Wsign-compare on.

Choosing unsigned because "it cannot be negative". That reasoning ignores what happens when the value meets a signed one. Use unsigned when you want modulo arithmetic or when a specification demands the width — not as a comment on the quantity's sign.

9. Interview Relevance

This chapter carries a Critical weighting, and the reason is specific.

"What is the difference between signed and unsigned?" is rarely a question about ranges. The interviewer is checking whether you know that the two have different behaviour when a calculation leaves the range, and whether you can say which one is undefined. A candidate who answers only "unsigned cannot hold negative numbers" has described the easy half.

A strong answer names the asymmetry — unsigned wraps modulo 2ᴺ by guarantee, signed overflow is undefined — and then volunteers the mixed-comparison trap, because that is the one that shows up in real code.

10. What You Should Be Able to Say Now

  1. Every integer type has a signed and an unsigned form, the same size, with the bits interpreted differently.
  2. Unsigned spends its patterns on non-negative values; signed spends about half on negatives. Ask std::numeric_limits rather than memorising ranges.
  3. Unsigned arithmetic is modulo 2ᴺ and the wraparound is guaranteed. Measured: max + 1 is 0, and 0 − 1 is the maximum, identically at -O0 and -O2.
  4. An unsigned value can never be negative, so subtracting past zero gives a very large positive number.
  5. The modulus follows the type's width — a std::uint8_t wraps at 256.
  6. Signed overflow is undefined behaviour. It is not wraparound. The standard says nothing about the result.
  7. No warning appeared for signed overflow at -Wall -Wextra -Wpedantic; -fsanitize=undefined reported it precisely at run time.
  8. The asymmetry exists because assuming signed overflow cannot happen lets the compiler optimise — and that assumption becomes false exactly when you overflow.
  9. Unsigned wraparound is a tool you may use deliberately. Signed overflow is always a bug.
  10. -1 < 4u is false: the signed value converts to unsigned first. Every step is defined; the answer is still wrong.
  11. -Wsign-compare catches that at compile time.
  12. "Unsigned" is better understood as "modulo 2ᴺ arithmetic" than as "cannot be negative".

11. Exercises

Exercise 1 — defined or undefined? For each, say whether the behaviour is defined, and what it is if so: (a) unsigned int u = 0; u = u - 1; (b) int i = INT_MAX; i = i + 1; (c) std::uint8_t b = 255; b = b + 1; (d) int i = -5; i = i - 1;

Exercise 2 — predict the comparison. Say what each prints and why: (a) int a = -1; unsigned b = 1; a < b (b) int a = -1; int b = 1; a < b. Then name the flag that warns about the first.

Exercise 3 — the loop that never ends. Explain why this does not terminate, and give two different fixes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (unsigned int i = 4; i >= 0; --i) { /* ... */ }

Exercise 4 — argue against a colleague. Someone says: "A buffer length can never be negative, so it should be unsigned." Give the strongest case for their position, then the specific failure from §7 that complicates it.

Exercise 5 — why no warning? Signed overflow produced no compiler diagnostic. Explain why a compiler finds this hard to warn about in general, and name the tool that detected it and when it did so.

Exercise 6 — deliberate wrapping. A free-running 16-bit sequence number must cycle from 65535 back to 0. Choose a type, say whether the wrap is guaranteed, and say what you would write in the source so a reviewer knows the wrap is intended.

Exercise 7 — explain the stability. The unsigned program gave identical output at -O0 and -O2. Explain why that was expected, and why the same expectation would be unsound for the signed program.

12. Summary

  • Every integer type has two forms, the same size, with different interpretation of the bits.
  • Signedness is a spending decision on a fixed number of patterns: all non-negative, or roughly half negative.
  • Unsigned arithmetic wraps modulo 2ᴺ, guaranteed. Max + 1 is 0; 0 − 1 is max; a uint8_t wraps at 256.
  • Signed overflow is undefined behaviour and is not wraparound. No result should be predicted or quoted.
  • The compiler gave no warning for it; the UB sanitizer named the file, line and arithmetic.
  • The asymmetry buys optimisation — and costs you when the assumption is violated.
  • -1 < 4u is false. Defined at every step, and still the wrong answer. -Wsign-compare catches it.
  • Unsigned means modulo-2ᴺ arithmetic, not "cannot be negative". Prefer signed for ordinary arithmetic; use unsigned for deliberate wrapping or a specified width.

Choosing signedness deliberately is half of choosing an integer type well. The other half is the width — and Chapter 3.2 left you with a problem there, because int and long are different sizes on different platforms, and a value that came out of a 32-bit register is 32 bits everywhere regardless of what the host machine prefers. Chapter 3.4 is about the types that say their width out loud, why they are the right default for anything that touches hardware, and the one surprising thing that happens when you print the smallest of them.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.