Value Representation
Value representation defines how an instance of a Rust type is encoded as a sequence of bytes in memory, which bit patterns are valid for that type, and whether any bytes remain uninitialized. Every value of a Sized type T occupies std::mem::size_of::<T>() contiguous bytes in memory.
Byte-Level Encoding of Primitives
Primitive types have fixed, architecture-defined bit representations:
| Type | Size (Bytes) | Valid Bit Patterns | Representation |
|---|---|---|---|
u8, u16, u32, u64, u128 |
1, 2, 4, 8, 16 | All bit patterns (0..=2^N - 1) |
Unsigned binary (native endianness) |
i8, i16, i32, i64, i128 |
1, 2, 4, 8, 16 | All bit patterns | Two's complement (native endianness) |
f32, f64 |
4, 8 | All bit patterns | IEEE 754 binary32 / binary64 |
bool |
1 | 0x00 (false) or 0x01 (true) only |
Single byte; 0x02..=0xFF are invalid |
char |
4 | 0x0000..=0xD7FF and 0xE000..=0x10FFFF |
32-bit Unicode scalar value |
&T, &mut T, NonNull<T> |
8 (on 64-bit) | Non-null (!= 0), aligned for T |
Virtual memory address |
*const T, *mut T |
8 (on 64-bit) | Any initialized bit pattern (including 0) |
Raw memory address |
() |
0 | Single trivial state | Zero bytes |
Endianness
Multi-byte integers and char values are stored in the target CPU's native endianness. On little-endian architectures (x86_64, AArch64, RISC-V, WebAssembly), the least significant byte is stored at the lowest memory address.
Value: 0x12345678_u32
Little-Endian Memory Layout (lowest address first):
+------+------+------+------+
| 0x78 | 0x56 | 0x34 | 0x12 |
+------+------+------+------+
+0 +1 +2 +3
Explicit byte conversion methods (to_le_bytes, to_be_bytes, to_ne_bytes) expose the byte representation without unsafe code:
fn main() {
let number: u32 = 0x1234_5678;
println!("Native bytes: {:02x?}", number.to_ne_bytes());
println!("Little-endian: {:02x?}", number.to_le_bytes());
println!("Big-endian: {:02x?}", number.to_be_bytes());
}
Native bytes: [78, 56, 34, 12]
Little-endian: [78, 56, 34, 12]
Big-endian: [12, 34, 56, 78]
Validity Invariants and Uninitialized Memory
A type's validity invariant specifies which byte states the compiler assumes a value can hold at all times. Constructing a value whose bytes violate its validity invariant causes immediate undefined behavior, even if the value is never read.
- Uninitialized bytes: Before memory is written, its bytes are conceptually
undef(uninitialized), not an arbitrary fixed integer. Reading uninitialized bytes as an integer (u8) or boolean (bool) is undefined behavior.std::mem::MaybeUninit<T>is the only safe wrapper for holding potentially uninitialized bytes. - Padding bytes: Padding bytes between struct fields are treated as uninitialized when a struct is copied or moved.
- Bit-validity:
u8accepts any initialized 8-bit value (0x00..=0xFF), whereasboolonly accepts0x00and0x01.
use std::mem::MaybeUninit;
fn main() {
let mut slot: MaybeUninit<u32> = MaybeUninit::uninit();
slot.write(0xDEAD_BEEF);
// SAFETY: `slot` was initialized by `slot.write` above.
let initialized: u32 = unsafe { slot.assume_init() };
println!("initialized = {:#x}", initialized);
}
initialized = 0xdeadbeef
Zero-Sized Types (ZSTs)
A zero-sized type (ZST) has a size of 0 bytes (size_of::<T>() == 0). Unit types (()), empty structs (struct Marker;), arrays of length zero ([T; 0]), and PhantomData<T> occupy no space in memory and generate no load or store instructions at runtime.
use std::mem::size_of;
struct EmptyMarker;
fn main() {
let markers = [EmptyMarker, EmptyMarker, EmptyMarker];
println!("size of EmptyMarker: {} bytes", size_of::<EmptyMarker>());
println!("size of [EmptyMarker; 3]: {} bytes", size_of_val(&markers));
println!("size of [u64; 0]: {} bytes", size_of::<[u64; 0]>());
}
size of EmptyMarker: 0 bytes
size of [EmptyMarker; 3]: 0 bytes
size of [u64; 0]: 0 bytes