Langsung ke konten
Semua insights

Academy

The Unit Type, Sizes, and Alignment: What Rust Learned from void's Mistakes

Rust’s unit type `()` is a zero-sized, composable representation of "nothing," replacing `void` and `void*` while enforcing complete type safety.

7 menit baca

The Unit Type, Sizes, and Alignment: What Rust Learned from void's Mistakes
Di halaman ini

Every C programmer knows void. It's the type that means "nothing"—a function that returns void doesn't return a value. A void* is a pointer to "something, but we're not saying what." It's a useful concept that C implemented in the worst possible way. You can't create a variable of type void. You can't put void in an array. You can't have a reference to void. And void* is a type-safety escape hatch that lets you cast any pointer to any other pointer without the compiler complaining, which is how approximately 30% of C's security vulnerabilities are born.

Rust replaced void with the unit type ()—and the difference is more significant than it appears. () is a real type. It has exactly one value (also written ()). You can store it in a variable, return it from a function, put it in a collection, and use it as a generic parameter. It's not a special case or a compiler hack—it's a regular type that happens to carry no information, which makes it composable with every other feature in the language.

Beyond the unit type, understanding how Rust handles type sizes and memory alignment is essential for anyone who cares about performance, interoperability with C libraries, or memory-efficient data structures. Sizes determine how much memory your data consumes. Alignment determines how that memory is accessed. Together, they define the physical layout of your program's data—the layer where your abstractions meet the hardware.

At RantAI, where we optimize data layouts for cache performance in our simulation engines and maintain C FFI interfaces for our AI inference pipelines, sizes and alignment aren't abstract concepts—they're daily engineering concerns. This article, drawn from Chapter 8, Sections 8.4 through 8.6 of our guide "The Rust Programming Language," covers the unit type, type sizes, and memory alignment.

The Unit Type: void Done Right#

Rust
// Functions that return nothing use ()
fn greet(name: &str) {
    println!("Hello, {}!", name);
}  // Implicit return type is ()

// Equivalent explicit declaration
fn greet_explicit(name: &str) -> () {
    println!("Hello, {}!", name);
}

// () is a real value you can use
fn main() {
    let nothing: () = ();
    let also_nothing = greet("Alice");  // also_nothing is ()
    
    // () works with generics — no special cases needed
    let result: Result<(), String> = Ok(());
    let optional: Option<()> = Some(());
    
    // Vec<()> is valid (though not very useful)
    let units: Vec<()> = vec![(), (), ()];
    println!("Size of (): {}", std::mem::size_of::<()>());  // 0!
}

The fact that () has zero size is important: Result<(), Error> doesn't waste any memory on the success case. Option<()> is just a boolean. The compiler optimizes these zero-sized types away entirely—they exist in the type system for correctness but generate no runtime code or memory overhead.

Compare this to C's void, which can't be used as a generic parameter. In C, you can't have Result<void, Error> because void isn't a real type. You'd need a separate Result_void type or a union with a dummy field. In Rust, () just works with everything because it's a regular type.

No void* — And Good Riddance#

Rust doesn't have void*. Instead:

Rust
// For type-erased pointers (rare, usually in FFI)
let raw: *const std::ffi::c_void = std::ptr::null();

// For "any type" in safe code — use generics or trait objects
fn print_anything(item: &dyn std::fmt::Display) {
    println!("{}", item);
}

// For "I don't know the type yet" — use generics
fn identity<T>(x: T) -> T { x }

Type Sizes: size_of and Why Every Byte Counts#

Rust
use std::mem;

fn main() {
    // Fundamental type sizes
    println!("bool:  {} byte", mem::size_of::<bool>());   // 1
    println!("u8:    {} byte", mem::size_of::<u8>());     // 1
    println!("i16:   {} bytes", mem::size_of::<i16>());   // 2
    println!("i32:   {} bytes", mem::size_of::<i32>());   // 4
    println!("i64:   {} bytes", mem::size_of::<i64>());   // 8
    println!("i128:  {} bytes", mem::size_of::<i128>());  // 16
    println!("f32:   {} bytes", mem::size_of::<f32>());   // 4
    println!("f64:   {} bytes", mem::size_of::<f64>());   // 8
    println!("char:  {} bytes", mem::size_of::<char>());  // 4
    println!("usize: {} bytes", mem::size_of::<usize>()); // 4 or 8
    println!("():    {} bytes", mem::size_of::<()>());    // 0!
    
    // Compound type sizes
    println!("&str:     {} bytes", mem::size_of::<&str>());      // 16 (ptr + len)
    println!("String:   {} bytes", mem::size_of::<String>());    // 24 (ptr + len + cap)
    println!("Vec<i32>: {} bytes", mem::size_of::<Vec<i32>>());  // 24 (ptr + len + cap)
    println!("Option<i32>: {} bytes", mem::size_of::<Option<i32>>()); // 8 (i32 + discriminant + padding)
    
    // Compile-time size assertions
    const _: () = assert!(mem::size_of::<i32>() == 4);
    const _: () = assert!(mem::size_of::<usize>() >= 4);
}

Compile-time size assertions are invaluable for FFI code, binary protocols, and any situation where your program's correctness depends on specific type sizes. If the assertion fails, the build fails—not the program.

Alignment: Why Your Struct Has Invisible Padding#

Rust
use std::mem;

struct Naive {
    a: u8,   // 1 byte
    b: u64,  // 8 bytes
    c: u8,   // 1 byte
}
// Size: NOT 10 bytes! It's 24 bytes due to alignment padding.

struct Optimized {
    b: u64,  // 8 bytes (aligned to 8-byte boundary)
    a: u8,   // 1 byte
    c: u8,   // 1 byte
    // 6 bytes of trailing padding to reach 8-byte alignment
}
// Size: 16 bytes — 8 bytes saved just by reordering fields!

fn main() {
    println!("Naive size: {}, align: {}", 
        mem::size_of::<Naive>(), mem::align_of::<Naive>());
    println!("Optimized size: {}, align: {}", 
        mem::size_of::<Optimized>(), mem::align_of::<Optimized>());
    
    // Alignment requirements
    println!("align of u8:  {}", mem::align_of::<u8>());   // 1
    println!("align of u16: {}", mem::align_of::<u16>());  // 2
    println!("align of u32: {}", mem::align_of::<u32>());  // 4
    println!("align of u64: {}", mem::align_of::<u64>());  // 8
}

Alignment matters because CPUs access memory most efficiently (and sometimes only) at aligned addresses. A u64 needs to start at an address divisible by 8. If your struct places a u8 before a u64, the compiler inserts 7 bytes of padding after the u8 to align the u64. Reordering fields from largest to smallest minimizes this padding.

Rust is allowed to (and does, by default) reorder struct fields for optimal layout. If you need a specific layout (for FFI or binary protocols), use #[repr(C)] to force C-compatible ordering:

Rust
#[repr(C)]  // Don't reorder fields — match C's layout rules
struct FFIStruct {
    a: u8,
    b: u64,
    c: u8,
}

Broader Implications: Layout as Performance#

At RantAI, struct layout optimization is part of our performance toolkit. Our simulation engines store millions of entities, each represented by a struct. Reducing struct size from 48 bytes to 40 bytes across a million entities saves 8MB of memory and improves cache utilization. We use size_of assertions in our performance-critical types to catch accidental size regressions during refactoring.

Practical Applications & Strategic Takeaways#

For newcomers: () is Rust's "nothing" type. It's zero-sized and works everywhere. Don't fight it—embrace it, especially with Result<(), Error> for functions that can fail but don't return a value on success.

For C interop developers: Use #[repr(C)] for any struct that crosses the FFI boundary. Use size_of and align_of assertions to verify your Rust structs match the C layout.

For performance engineers: Profile your struct sizes. Reorder fields from largest to smallest alignment. Consider #[repr(packed)] (with caution) for memory-critical applications. Every byte of padding multiplied by millions of instances adds up.

Our Commitment to Open Knowledge#

RantAI is committed to open education. The unit type, sizes, and alignment are covered in Chapter 8, Sections 8.4–8.6 of our guide, "The Rust Programming Language," freely available online.

Explore these concepts further: https://trpl.rantai.dev

Support Our Mission & Get Your Handbook#

Have you ever saved significant memory by reordering struct fields? Or been surprised by how much padding a struct contained? Share your layout optimization stories below!

#RustLang #UnitType #MemoryLayout #Alignment #RantAI #LearnRust #SizeOf #StructLayout #Performance #SystemsProgramming

Artikel berikutnya

Floating-Point Types, Literals, and the Prefix/Suffix System