unsigned
integers have some strange properties and you should avoid them unless you have a good reason. Gaining 1 extra bit of positive size, or expressing a constraint that a value may not be negative, are not good reasons.
unsigned
integers implement arithmetic modulo UINT_MAX+1
. By contrast, operations on signed
integers represent the natural arithmetic that we are familiar with from school.
Overflow semantics
unsigned
has well defined overflow; signed
does not:
unsigned u = UINT_MAX;
u++; // u becomes 0
int i = INT_MAX;
i++; // undefined behaviour
This has the consequence that signed integer overflow can be caught during testing, while an unsigned overflow may silently do the wrong thing. So use unsigned
only if you are sure you want to legalize overflow.
If you have a constraint that a value may not be negative, then you need a way to detect and reject negative values; int
is perfect for this. An unsigned
will accept a negative value and silently overflow it into a positive value.
Bit shift semantics
Bit shift of unsigned
by an amount not greater than the number of bits in the data type is always well defined. Until C++20, bit shift of signed
was undefined if it would cause a 1 in the sign bit to be shifted left, or implementation-defined if it would cause a 1 in the sign bit to be shifted right. Since C++20, signed
right shift always preserves the sign, but signed
left shift does not. So use unsigned
for some kinds of bit twiddling operations.
Mixed sign operations
The built-in arithmetic operations always operate on operands of the same type. If they are supplied operands of different types, the "usual arithmetic conversions" coerce them into the same type, sometimes with surprising results:
unsigned u = 42;
std::cout << (u * -1); // 4294967254
std::cout << std::boolalpha << (u >= -1); // false
What's the difference?
Subtracting an unsigned
from another unsigned
yields an unsigned
result, which means that the difference between 2
and 1
is 4294967295
.
Double the max value
int
uses one bit to represent the sign of the value. unsigned
uses this bit as just another numerical bit. So typically, int
has 31 numerical bits and unsigned
has 32. This extra bit is often cited as a reason to use unsigned
. But if 31 bits are insufficient for a particular purpose, then most likely 32 bits will also be insufficient, and you should be considering 64 bits or more.
Function overloading
The implicit conversion from int
to unsigned
has the same rank as the conversion from int
to double
, so the following example is ill formed:
void f(unsigned);
void f(double);
f(42); // error: ambiguous call to overloaded function
Interoperability
Many APIs (including the standard library) use unsigned
types, often for misguided reasons. It is sensible to use unsigned
to avoid mixed-sign operations when interacting with these APIs.
Appendix
The quoted snippet includes the expression 0 <= grade <= 100
. This will first evaluate 0 <= grade
, which is always true
, because grade
can't be negative. Then it will evaluate true <= 100
, which is always true
, because true
is converted to the integer 1
, and 1 <= 100
is true
.