Signed and Unsigned Integers
Every integer type in Rust belongs to one of two families: signed integers (i8, i16, i32, i64, i128, isize), which can represent negative, zero, and positive whole numbers, and unsigned integers (u8, u16, u32, u64, u128, usize), which can represent only zero and positive whole numbers. Both families occupy the exact same bit widths in memory, differing only in how the processor and compiler interpret their bit patterns.
Paired Bit Widths and Value Ranges
For every bit width $n$, an unsigned integer uses all $n$ bits for magnitude, yielding a range of $0$ to $2^n - 1$. A signed integer reserves one bit for the sign, splitting the $2^n$ possible bit patterns evenly between negative values ($-2^{n-1}$ to $-1$) and non-negative values ($0$ to $2^{n-1} - 1$).
| Bit Width | Signed Type | Signed Range (MIN..=MAX) |
Unsigned Type | Unsigned Range (MIN..=MAX) |
|---|---|---|---|---|
| 8-bit | i8 |
-128 to 127 |
u8 |
0 to 255 |
| 16-bit | i16 |
-32_768 to 32_767 |
u16 |
0 to 65_535 |
| 32-bit | i32 |
-2_147_483_648 to 2_147_483_647 |
u32 |
0 to 4_294_967_295 |
| 64-bit | i64 |
-9.22 × 10¹⁸ to 9.22 × 10¹⁸ |
u64 |
0 to 1.84 × 10¹⁹ |
| 128-bit | i128 |
-1.70 × 10³⁸ to 1.70 × 10³⁸ |
u128 |
0 to 3.40 × 10³⁸ |
| Pointer-sized | isize |
-2³¹..2³¹-1 (32-bit) or -2⁶³..2⁶³-1 (64-bit) |
usize |
0..2³²-1 (32-bit) or 0..2⁶⁴-1 (64-bit) |
fn main() {
println!("i8: {} to {}", i8::MIN, i8::MAX);
println!("u8: {} to {}", u8::MIN, u8::MAX);
println!("i16: {} to {}", i16::MIN, i16::MAX);
println!("u16: {} to {}", u16::MIN, u16::MAX);
}
i8: -128 to 127
u8: 0 to 255
i16: -32768 to 32767
u16: 0 to 65535
Binary Representation: Magnitude vs Two's Complement
Unsigned integers use pure base-2 positional notation. Signed integers in Rust are guaranteed by the language specification to use two's complement representation on all platforms.
In an $n$-bit two's complement integer, the most significant bit (MSB, bit $n-1$) has a negative weight of $-2^{n-1}$, while the remaining $n-1$ bits have positive weights ($2^{n-2}$ down to $2^0$):
8-bit Unsigned (u8): all 8 bits carry positive weight
+------+------+------+------+------+------+------+------+
| +128 | +64 | +32 | +16 | +8 | +4 | +2 | +1 |
+------+------+------+------+------+------+------+------+
| 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | = 128 + 64 + ... + 1 = 255
+------+------+------+------+------+------+------+------+
8-bit Signed (i8): MSB carries negative weight (-128)
+------+------+------+------+------+------+------+------+
| -128 | +64 | +32 | +16 | +8 | +4 | +2 | +1 |
+------+------+------+------+------+------+------+------+
| 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | = -128 + 127 = -1
+------+------+------+------+------+------+------+------+
| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | = -128 + 0 = -128 (i8::MIN)
+------+------+------+------+------+------+------+------+
| 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | = 0 + 127 = +127 (i8::MAX)
+------+------+------+------+------+------+------+------+
For bit patterns from 0b0000_0000 (0) through 0b0111_1111 (127), u8 and i8 have identical binary representations and numeric values. Only bit patterns with the MSB set to 1 (0b1000_0000 through 0b1111_1111) differ in meaning:
| Binary Pattern | Hex | Value as u8 |
Value as i8 |
|---|---|---|---|
0b0000_0000 |
0x00 |
0 |
0 |
0b0000_0001 |
0x01 |
1 |
1 |
0b0111_1111 |
0x7F |
127 |
127 (i8::MAX) |
0b1000_0000 |
0x80 |
128 |
-128 (i8::MIN) |
0b1000_0001 |
0x81 |
129 |
-127 |
0b1111_1110 |
0xFE |
254 |
-2 |
0b1111_1111 |
0xFF |
255 (u8::MAX) |
-1 |
Two's complement has no negative zero (-0), which means every signed type has one more negative number than positive numbers: |MIN| == MAX + 1.
The Asymmetry of MIN and unsigned_abs
Because a signed integer's minimum value has no positive counterpart of the same signed type (for example, +128 cannot fit in i8), negating MIN or calling .abs() on MIN overflows:
fn main() {
let min_val: i8 = i8::MIN; // -128
// Calling `min_val.abs()` or `-min_val` panics in debug mode!
// Instead, `.unsigned_abs()` returns the magnitude as the matching unsigned type (`u8`):
let magnitude: u8 = min_val.unsigned_abs();
println!("|{min_val}| as u8 = {magnitude}");
let checked: Option<i8> = min_val.checked_abs();
println!("checked_abs of {min_val} = {checked:?}");
}
|-128| as u8 = 128
checked_abs of -128 = None
unsigned_abs() is guaranteed never to overflow for any signed value, including MIN, because the magnitude of iN::MIN ($2^{n-1}$) always fits within uN (whose maximum is $2^n - 1$).
Sign-Specific Operations and Methods
Signed types support the unary negation operator (-) and sign-inspection methods that are not defined on unsigned types:
| Operation / Method | Signed (i8..isize) |
Unsigned (u8..usize) |
Description |
|---|---|---|---|
-x |
Valid | Compile error (E0600) |
Negates the value |
x.abs() |
Returns signed type | Not defined | Magnitude as same signed type (panics on MIN in debug) |
x.unsigned_abs() |
Returns unsigned counterpart | Not defined | Magnitude as matching unsigned type (never panics) |
x.signum() |
Returns -1, 0, or 1 |
Not defined | Sign indicator of the integer |
x.is_negative() |
true if x < 0 |
Not defined | Tests whether the sign bit is set |
x.is_positive() |
true if x > 0 |
Not defined | Tests whether the value is strictly greater than zero |
fn main() {
let delta: i32 = -42;
println!("negated: {}", -delta);
println!("abs: {}", delta.abs());
println!("signum: {}", delta.signum());
println!("is_negative: {}", delta.is_negative());
println!("is_positive: {}", delta.is_positive());
}
negated: 42
abs: 42
signum: -1
is_negative: true
is_positive: false
Applying the unary - operator to an unsigned integer fails at compile time:
// ❌ does not compile
fn main() {
let count: u32 = 10;
let negated = -count;
}
error[E0600]: cannot apply unary operator `-` to type `u32`
--> src/main.rs:3:19
|
3 | let negated = -count;
| ^^^^^^ cannot apply unary operator `-`
|
= note: unsigned values cannot be negated
error: aborting due to 1 previous error
Choosing Between Signed and Unsigned Types
| Use Case | Recommended Family | Typical Type | Reason |
|---|---|---|---|
Array, slice, and Vec indexing |
Unsigned | usize |
Memory addresses and collection lengths are never negative |
| Object counts and capacities | Unsigned | usize, u32, u64 |
The type itself documents and enforces the >= 0 invariant |
| Raw binary data, network packets, UTF-8 bytes | Unsigned | u8 |
Bytes represent raw 8-bit patterns (0x00..0xFF), not signed quantities |
| Bitmasks, bitwise shifts, and flags | Unsigned | u32, u64, u128 |
Avoids sign-extension behavior during right shifts (>>) |
| General arithmetic, loops, and deltas | Signed | i32, i64 |
Subtraction (a - b) naturally produces negative results without underflow |
| Pointer offsets and relative distances | Signed | isize |
Represents backward (negative) or forward (positive) memory offsets |
Right Shift (>>) Behavior Difference
The right-shift operator (>>) behaves differently on signed and unsigned integers:
- Unsigned (
u8..usize): Performs a logical right shift, filling vacated high-order bits with0. - Signed (
i8..isize): Performs an arithmetic right shift, filling vacated high-order bits with copies of the sign bit (1for negative numbers,0for non-negative numbers) to preserve the number's sign.
fn main() {
let unsigned_byte: u8 = 0b1000_0000; // 128
let signed_byte: i8 = -128; // 0b1000_0000 in two's complement
println!("u8 >> 2: {:08b} ({})", unsigned_byte >> 2, unsigned_byte >> 2);
println!("i8 >> 2: {:08b} ({})", (signed_byte >> 2) as u8, signed_byte >> 2);
}
u8 >> 2: 00100000 (32)
i8 >> 2: 11100000 (-32)
Mixing Signed and Unsigned Values
Unlike C and C++, which silently convert signed integers to unsigned integers when both appear in the same expression (causing -1 < 1u to evaluate to false in C), Rust rejects any binary arithmetic or comparison between signed and unsigned types:
// ❌ does not compile
fn main() {
let signed: i32 = -5;
let unsigned: u32 = 10;
if signed < unsigned {
println!("smaller");
}
}
error[E0308]: mismatched types
--> src/main.rs:5:17
|
5 | if signed < unsigned {
| ^^^^^^^^ expected `i32`, found `u32`
|
help: you can convert a `u32` to an `i32` and panic if the converted value doesn't fit
|
5 | if signed < unsigned.try_into().unwrap() {
| ++++++++++++++++++++
error: aborting due to 1 previous error
Safe Mixed-Sign Arithmetic Methods
When you need to add an isize offset to a usize index (or add a signed delta to any unsigned coordinate), standard library methods perform the mixed-sign operation directly without lossy casts:
fn main() {
let index: usize = 10;
let backward_step: isize = -3;
let too_far_back: isize = -20;
println!("checked_add_signed(-3): {:?}", index.checked_add_signed(backward_step));
println!("checked_add_signed(-20): {:?}", index.checked_add_signed(too_far_back));
println!("saturating_add_signed: {}", index.saturating_add_signed(too_far_back));
let position: i32 = -5;
let distance: u32 = 12;
println!("checked_add_unsigned: {:?}", position.checked_add_unsigned(distance));
}
checked_add_signed(-3): Some(7)
checked_add_signed(-20): None
saturating_add_signed: 0
checked_add_unsigned: Some(7)
Reinterpreting Bits Between Signed and Unsigned (cast_signed and cast_unsigned)
To reinterpret the underlying bits of an integer between its signed and unsigned counterparts of the exact same width without changing the bit pattern, use .cast_signed() and .cast_unsigned() (or checked conversion via TryFrom when the numeric value itself must be preserved):
fn main() {
let raw: u8 = 250;
// Bitwise reinterpretation (same 8 bits: 0b1111_1010 -> -6)
let bits_as_signed: i8 = raw.cast_signed();
let back_to_unsigned: u8 = bits_as_signed.cast_unsigned();
println!("cast_signed: {raw}u8 -> {bits_as_signed}i8 -> {back_to_unsigned}u8");
// Value-preserving conversion (fails because 250 > i8::MAX)
let value_checked = i8::try_from(raw);
println!("i8::try_from(250u8) = {value_checked:?}");
}
cast_signed: 250u8 -> -6i8 -> 250u8
i8::try_from(250u8) = Err(TryFromIntError(()))