Layout knowledge matters for FFI, unsafe code, cache-friendly data structures and understanding why Option<&T> is free.
Every type has a size and an alignment; the size is always a multiple of the alignment so that arrays work. std::mem::size_of::<T>() and align_of::<T>() report them.
With the default representation, `repr(Rust)`, the compiler may reorder fields to minimise padding, and the layout is unspecified: it can change between compiler versions. Never assume field order or offsets for a plain struct.
Other representations give guarantees:
- `#[repr(C)]`: fields in declaration order with C padding rules. Required for FFI and for any layout you rely on from unsafe code.
- `#[repr(transparent)]`: a struct with one non-zero-sized field has exactly that field's layout and ABI. Ideal for newtypes passed across FFI.
- `#[repr(u8)]` (or another integer) on an enum fixes the discriminant type and values.
- `#[repr(packed)]` removes padding, but fields may be misaligned, so taking references to them is an error; read them by copy or through
&raw const. - `#[repr(align(64))]` raises alignment, e.g. to keep hot atomics on separate cache lines.
Niche optimisation: when a type has invalid bit patterns, enums use them to store the discriminant. References and Box are never null, so Option<&T> and Option<Box<T>> are the size of a pointer, with None represented as null. NonZeroU32 gives the same benefit for integers, and bool and char have niches too.
Zero-sized types ((), unit structs, PhantomData) take no space, and a Vec<()> never allocates. Dynamically sized types (str, [T], dyn Trait) can only be used behind pointers, which become fat: pointer plus length or pointer plus vtable.
Enums are as large as their largest variant plus a discriminant (unless a niche absorbs it). Boxing a rarely used large variant keeps the whole enum small; clippy's large_enum_variant lint flags this.
Example
use std::mem::{align_of, size_of};
use std::num::NonZeroU32;
#[allow(dead_code)] struct Loose { a: u8, b: u32, c: u16 } // repr(Rust): reordered
#[allow(dead_code)] #[repr(C)] struct CLike { a: u8, b: u32, c: u16 } // declaration order
#[repr(transparent)] struct UserId(#[allow(dead_code)] u64);
#[allow(dead_code)] enum Big { Small(u8), Huge([u8; 1024]) }
#[allow(dead_code)] enum Slim { Small(u8), Huge(Box<[u8; 1024]>) }
fn main() {
println!("Loose {} / CLike {} (align {})", size_of::<Loose>(), size_of::<CLike>(), align_of::<CLike>());
// Loose 8 / CLike 12 (align 4)
println!("UserId {}", size_of::<UserId>()); // 8
println!("&u8 {} / Option<&u8> {}", size_of::<&u8>(), size_of::<Option<&u8>>()); // 8 / 8
println!("Option<u32> {} / Option<NonZeroU32> {}",
size_of::<Option<u32>>(), size_of::<Option<NonZeroU32>>()); // 8 / 4
println!("&str {} / &dyn Fn() {}", size_of::<&str>(), size_of::<&dyn Fn()>()); // 16 / 16
println!("() {} / Big {} / Slim {}", size_of::<()>(), size_of::<Big>(), size_of::<Slim>()); // 0 / 1025 / 16
}Remember
- Default layout is unspecified and may reorder fields; use
repr(C)when layout matters - Niche optimisation makes
Option<&T>,Option<Box<T>>,Option<NonZeroU32>free - DST pointers are fat; ZSTs take no space; box large rare enum variants
Watch out for
- Transmuting or casting between
repr(Rust)structs that look identical; their layouts are not guaranteed to match.