size_of, align_of and repr
A value in Rust occupies a number of bytes and starts at an address that is a multiple of its alignment. Both numbers are part of the type, and the standard library reports them with size_of and align_of. They are the numbers behind a buffer, a file header and every boundary to C.
Coming from C++? Memory layout builds the same struct with both compilers, prints sizeof and the offsets, and shows why the two answers differ.
This lesson measures the primitive types, then shows that a Rust struct does not keep the order it was written in, which is the surprise for anyone who has read a C struct as a byte map. The rest of the lesson is the three repr attributes that take the choice back: one for a layout C can read, one for a newtype that disappears, and one that packs a struct and makes a reference into it a compile error.
Size and alignment
An integer is as wide as its name says, and the alignment equals the size for the integer types. Two types are different: char is four bytes because it holds a Unicode scalar value, and bool is one byte even though it has two values.
The program prints the size and the alignment of the types a buffer and a file header are built from. The container types at the end are the interesting line: a String and a Vec are three words, not the text or the elements they own.
use std::mem::{align_of, size_of};
fn main() {
println!("i32 size {} align {}", size_of::<i32>(), align_of::<i32>());
println!("u8 size {} align {}", size_of::<u8>(), align_of::<u8>());
println!("u64 size {} align {}", size_of::<u64>(), align_of::<u64>());
println!("char size {} align {}", size_of::<char>(), align_of::<char>());
println!("bool size {} align {}", size_of::<bool>(), align_of::<bool>());
println!("[i32; 4] size {}", size_of::<[i32; 4]>());
println!("&i32 size {}", size_of::<&i32>());
println!("String size {}", size_of::<String>());
println!("Vec<i32> size {}", size_of::<Vec<i32>>());
}
rustc --edition 2024 sizes.rs
./sizes
i32 size 4 align 4
u8 size 1 align 1
u64 size 8 align 8
char size 4 align 4
bool size 1 align 1
[i32; 4] size 16
&i32 size 8
String size 24
Vec<i32> size 24
An array is its elements laid end to end, so four integers are sixteen bytes with no header. A reference is one machine word, and String and Vec are a pointer and two lengths, which is why moving one of them copies twenty-four bytes and leaves the buffer where it was. The lesson on Vec shows the same three words in use.
The declaration order is not the layout
A Rust struct has no defined layout unless it says so. The compiler is free to reorder the fields to spend fewer bytes on padding, and it does: the struct below is written with a byte, a word and a byte, and it measures eight bytes, not twelve.
The offsets make the reordering visible. value is at offset zero, which is the third field of the source, and the two bytes sit at four and five.
use std::mem::{align_of, offset_of, size_of};
struct Mixed {
flag: u8,
value: u32,
tail: u8,
}
fn main() {
println!("size {} align {}", size_of::<Mixed>(), align_of::<Mixed>());
println!(
"flag {} value {} tail {}",
offset_of!(Mixed, flag),
offset_of!(Mixed, value),
offset_of!(Mixed, tail),
);
}
rustc --edition 2024 reorder.rs
./reorder
size 8 align 4
flag 4 value 0 tail 5
Eight bytes instead of twelve, and the price is that the offsets are an implementation detail. Some programs want the bytes in a known order more than they want the four saved bytes: a file format, a network message, or a struct that C also declares. Those add #[repr(C)] in the next section. The lesson on structs builds the type itself.
repr(C) for a defined layout
#[repr(C)] asks for the layout a C compiler would choose: fields in declaration order, each at the next offset that satisfies its alignment, and the whole struct rounded up to its largest alignment. The attribute is what a struct needs before it crosses to C, and it is also how a program documents an on-disk header.
The same three fields under repr(C) measure twelve bytes. The byte at offset one to three is padding, and nothing may read it as a value.
use std::mem::{offset_of, size_of};
#[repr(C)]
struct Header {
tag: u8,
length: u32,
flags: u8,
}
fn main() {
println!("size {}", size_of::<Header>());
println!(
"tag {} length {} flags {}",
offset_of!(Header, tag),
offset_of!(Header, length),
offset_of!(Header, flags),
);
}
rustc --edition 2024 header.rs
./header
size 12
tag 0 length 4 flags 8
The offsets are the declaration order now: zero, four and eight, with three bytes of padding after the tag and three at the end. A packed header saves those bytes at the cost of slower field access, which the last section turns into a compile error. The lesson on calling C is where this attribute becomes mandatory.
Enums, niches and repr(transparent)
An enum of variants without data is a tag, so three variants fit in one byte. An enum with data needs a tag plus the largest variant, rounded to its alignment, which is why the two-double variant sets the size of the whole type.
Option is where the layout gets clever. A reference can never be zero, so the compiler uses zero as None and keeps the type one word wide; the same trick works for i32. An Option<u8> has no spare bit, so it pays for a tag and measures two bytes.
use std::mem::size_of;
enum Colour {
Red,
Green,
Blue,
}
enum Shape {
Circle(f64),
Square(f64),
Pair(f64, f64),
}
#[repr(transparent)]
struct Metres(f64);
fn main() {
println!("Colour {}", size_of::<Colour>());
println!("Shape {}", size_of::<Shape>());
println!("Option<&u8> {}", size_of::<Option<&u8>>());
println!("&u8 {}", size_of::<&u8>());
println!("Option<i32> {}", size_of::<Option<i32>>());
println!("Option<u8> {}", size_of::<Option<u8>>());
println!("Metres {}", size_of::<Metres>());
println!("f64 {}", size_of::<f64>());
}
rustc --edition 2024 niche.rs
./niche
Colour 1
Shape 24
Option<&u8> 8
&u8 8
Option<i32> 8
Option<u8> 2
Metres 8
f64 8
The four lines around the reference are the whole idea of a niche: a type that cannot use every bit pattern lends the spare ones to Option, and the wrapper costs nothing. #[repr(transparent)] makes the same promise for a single-field struct: a Metres has the size, the alignment and the calling convention of its f64, which is how a newtype stays free at a boundary. The lesson on Option uses the type this layout makes cheap.
A reference into a packed struct
#[repr(packed)] removes all padding, so a field can start at an address its type does not allow. Taking a reference to that field would create a misaligned reference, which the compiler refuses with E0793 rather than trusting the program.
The program takes a reference to the four-byte field of a packed struct that starts at offset one. The compiler stops at the borrow, and the help line names the two replacements: read_unaligned and write_unaligned.
#[repr(packed)]
struct Packed {
flag: u8,
value: u32,
}
fn main() {
let packed = Packed { flag: 1, value: 42 };
let reference = &packed.value;
println!("{}", reference);
}
rustc --edition 2024 packed_err.rs
error[E0793]: reference to field of packed struct is unaligned
--> packed_err.rs:9:21
|
9 | let reference = &packed.value;
| ^^^^^^^^^^^^^
|
= note: this struct is 1-byte aligned, but the type of this field may require higher alignment
= note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced)
= help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers)
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0793`.
The note says why the check exists: a reference is a promise of alignment, and a packed field cannot keep it, so even a reference that is never read is undefined behaviour. Copying the field into a local reads it by value, which is defined, and a raw pointer with read_unaligned is the form that avoids the copy. The lesson on uninitialised memory continues with the rules a value must satisfy before it is read.
Exercises
- Print the size and alignment of a tuple of three types and check them against the sum of the parts.
- Reorder the fields of a struct of your own and find the arrangement with the smallest size.
- Add #[repr(C)] to that struct and print the offsets again; explain every gap.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
align_of::<u64>()return?Answer
8
An eight-byte integer is aligned to eight bytes on this target.
-
Why does a Rust struct not keep its declaration order?
Answer
The layout is unspecified unless a repr attribute asks for one
Without repr(C) the compiler may reorder fields to reduce padding.
-
What does
#[repr(C)]guarantee for a struct?Answer
The field order and padding a C compiler would choose
The attribute fixes the layout so another language can read the same bytes.
-
Why is
Option<&u8>the same size as&u8?Answer
A reference cannot be zero, so zero encodes None
The unused null bit pattern becomes the None variant, which costs nothing.
-
What does this program print?
use std::mem::size_of; struct Pair { left: u8, right: u8, } fn main() { println!("{} {}", size_of::<Pair>(), size_of::<[u8; 4]>()); }Answer
2 4
Two bytes of data need no padding, and the array is four bytes.
-
What does this program print?
use std::mem::{offset_of, size_of}; #[repr(C)] struct Header { tag: u8, length: u32, } fn main() { println!("{} {}", size_of::<Header>(), offset_of!(Header, length)); }Answer
8 4
The length is aligned to four, so three bytes of padding follow the tag and the struct is eight bytes.
-
What does this program print?
use std::mem::size_of; fn main() { println!( "{} {}", size_of::<Option<u8>>(), size_of::<Option<i32>>(), ); }Answer
2 8
A byte has no spare bit pattern, so Option<u8> needs a tag, while i32 lends its unused patterns.
-
What does this program print?
use std::mem::size_of; #[repr(transparent)] struct Metres(f64); fn main() { println!("{} {}", size_of::<Metres>(), size_of::<f64>()); }Answer
8 8
repr(transparent) keeps the layout of the single field, so the newtype costs nothing.
-
What is the first line of the compiler output for this program?
#[repr(packed)] struct Packed { flag: u8, value: u32, } fn main() { let packed = Packed { flag: 1, value: 42 }; let reference = &packed.value; println!("{}", reference); }Answer
error[E0793]: reference to field of packed struct is unaligned
A packed field has no guaranteed alignment, so a reference to it is refused.
-
What is the first line of the compiler output for this program?
use std::mem::offset_of; struct Mixed { flag: u8, value: u32, } fn main() { println!("{}", offset_of!(Missing, value)); }Answer
error[E0425]: cannot find type
Missingin this scopeThe macro needs a type in scope, and Missing is not declared, which the compiler reports as an unresolved name.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev