4.5 — Unsigned integers, and why to avoid them
BY ALEX ON APRIL 23RD, 2019 | LAST MODIFIED BY NASCARDRIVER ON MARCH 17TH, 2020
Unsigned integers
In the previous lesson (4.4 -- Signed integers), we covered signed integers, which are a set of types that can hold positive and negative whole numbers, including 0.
C++ also supports unsigned integers. Unsigned integers are integers that can only hold non-negative whole numbers.
Defining unsigned integers
To define an unsigned integer, we use the unsigned keyword. By convention, this is placed before the type:
1
2
3
4
unsigned short us;
unsigned int ui;
unsigned long ul;
unsigned long long ull;
Unsigned integer range
A 1-byte unsigned integer has a range of 0 to 255. Compare this to the 1-byte signed integer range of -128 to 127. Both can store 256 different values, but signed integers use half of their range for negative numbers, whereas unsigned integers can store positive numbers that are twice as large.
Here’s a table showing the range for unsigned integers:
Size/Type
Range
1 byte unsigned
0 to 255
2 byte unsigned
0 to 65,535
4 byte unsigned
0 to 4,294,967,295
8 byte unsigned
0 to 18,446,744,073,709,551,615
An n-bit unsigned variable has a range of 0 to (2n)-1.
When no negative numbers are required, unsigned integers are well-suited for networking and systems with little memory, because unsigned integers can store more positive numbers without taking up extra memory.
Remembering the terms signed and unsigned
New programmers sometimes get signed and unsigned mixed up. The following is a simple way to remember the difference: in order to differentiate negative numbers from positive ones, we use a negative sign. If a sign is not provided, we assume a number is positive. Consequently, an integer with a sign (a signed integer) can tell the difference between positive and negative. An integer without a sign (an unsigned integer) assumes all values are positive.
Unsigned integer overflow
Trick question: What happens if we try to store the number 280 (which requires 9 bits to represent) in a 1-byte unsigned integer? You might think the answer is “overflow!”. But, it’s not.
By definition, unsigned integers cannot overflow. Instead, if a value is out of range, it is divided by one greater than the largest number of the type, and only the remainder kept.
The number 280 is too big to fit in our 1-byte range of 0 to 255. 1 greater than the largest number of the type is 256. Therefore, we divide 280 by 256, getting 1 remainder 24. The remainder of 24 is what is stored.
Here’s another way to think about the same thing. Any number bigger than the largest number representable by the type simply “wraps around” (sometimes called “modulo wrapping”). 255 is in range of a 1-byte integer, so 255 is fine. 256, however, is outside the range, so it wraps around to the value 0. 257 wraps around to the value 1. 280 wraps around to the value 24.
Let’s take a look at this using 2-byte integers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
int main()
{
unsigned short x{ 65535 }; // largest 16-bit unsigned value possible
std::cout << "x was: " << x << '\n';
x = 65536; // 65536 is out of our range, so we get wrap-around
std::cout << "x is now: " << x << '\n';
x = 65537; // 65537 is out of our range, so we get wrap-around
std::cout << "x is now: " << x << '\n';
return 0;
}
What do you think the result of this program will be?
x was: 65535
x is now: 0
x is now: 1
It’s possible to wrap around the other direction as well. 0 is representable in a 1-byte integer, so that’s fine. -1 is not representable, so it wraps around to the top of the range, producing the value 255. -2 wraps around to 254. And so forth.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
int main()
{
unsigned short x{ 0 }; // smallest 2-byte unsigned value possible
std::cout << "x was: " << x << '\n';
x = -1; // -1 is out of our range, so we get wrap-around
std::cout << "x is now: " << x << '\n';
x = -2; // -2 is out of our range, so we get wrap-around
std::cout << "x is now: " << x << '\n';
return 0;
}
x was: 0
x is now: 65535
x is now: 65534
Author's note
In common language, unsigned integer wrap around is often incorrectly called “overflow” since the cause is identical to signed integer overflow.
As an aside...
Many notable bugs in video game history happened due to wrap around behavior with unsigned integers. In the arcade game Donkey Kong, it’s not possible to go past level 22 due to a bug that leaves the user with not enough bonus time to complete the level. In the PC game Civilization, Gandhi was known for being the first one to use nuclear weapons, which seems contrary to his normally passive nature. Gandhi’s aggression setting was normally set at 1, but if he chose a democratic government, he’d get a -2 modifier. This wrapped around his aggression setting to 255, making him maximally aggressive!
The controversy over unsigned numbers
Many developers (and some large development houses, such as Google) believe that developers should generally avoid unsigned integers.
This is largely because of two behaviors that can cause problems.
First, consider the subtraction of two unsigned numbers, such as 3 and 5. 3 minus 5 is -2, but -2 can’t be represented as an unsigned number.
1
2
3
4
5
6
7
8
9
10
#include <iostream>
int main()
{
unsigned int x{ 3 };
unsigned int y{ 5 };
std::cout << x - y << '\n';
return 0;
}
On the author’s machine, this seemingly innocent looking program produces the result:
1
4294967294
This occurs due to -2 wrapping around to a number close to the top of the range of a 4-byte integer. A common unwanted wrap-around happens when an unsigned integer is repeatedly decremented with the -- operator. You’ll see an example of this when loops are introduced.
Second, unexpected behavior can result when you mix signed and unsigned integers. In the above example, even if one of the operands (x or y) is signed, the other operand (the unsigned one) will cause the signed one to be promoted to an unsigned integer, and the same behavior will result!
Consider the following snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
void doSomething(unsigned int x)
{
// Run some code x times
std::cout << "x is " << x << '\n';
}
int main()
{
doSomething(-1);
return 0;
}
The author of doSomething() was expecting someone to call this function with only positive numbers. But the caller is passing in -1. What happens in this case?
The signed argument of -1 gets implicitly converted to an unsigned parameter. -1 isn’t in the range of an unsigned number, so it wraps around to some large number (probably 4294967295). Then your program goes ballistic. Worse, there’s no good way to guard against this condition from happening. C++ will freely convert between signed and unsigned numbers, but it won’t do any range checking to make sure you don’t overflow your type.
If you need to protect a function against negative inputs, use an assertion or exception instead. Both are covered later.
Some modern programming languages (such as Java) and frameworks (such as .NET) either don’t include unsigned types, or limit their use.
New programmers often use unsigned integers to represent non-negative data, or to take advantage of the additional range. Bjarne Stroustrup, the designer of C++, said, “Using an unsigned instead of an int to gain one more bit to represent positive integers is almost never a good idea”.
Warning
Avoid using unsigned numbers, except in specific cases or when unavoidable.
Don’t avoid negative numbers by using unsigned types. If you need a larger range than a signed number offers, use one of the guaranteed-width integers shown in the next lesson (4.6 -- Fixed-width integers and size_t).
If you do use unsigned numbers, avoid mixing signed and unsigned numbers where possible.
So where is it reasonable to use unsigned numbers?
There are still a few cases in C++ where it’s okay (or necessary) to use unsigned numbers.
First, unsigned numbers are preferred when dealing with bit manipulation (covered in chapter O).
Second, use of unsigned numbers is still unavoidable in some cases, mainly those having to do with array indexing. We’ll talk more about this in the lessons on arrays and array indexing.
Also note that if you’re developing for an embedded system (e.g. an Arduino) or some other processor/memory limited context, use of unsigned numbers is more common and accepted (and in some cases, unavoidable) for performance reasons.