Generics

Generic Structs

A generic struct is a struct definition parameterized over one or more type parameters declared in angle brackets (<T>) after the struct name. Every field of the struct can use those type parameters directly or inside composite types such as Vec<T>, Option<T>, or (T, usize).

Named-Field and Tuple Generic Structs

Both named-field structs and tuple structs can declare type parameters:

#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

#[derive(Debug)]
struct Wrapper<T>(T);

fn main() {
    let integer_point = Point { x: 4, y: -9 };
    let float_point = Point { x: 1.5, y: 3.25 };
    let wrapped_text = Wrapper(String::from("payload"));

    println!("integer_point: ({}, {})", integer_point.x, integer_point.y);
    println!("float_point: ({}, {})", float_point.x, float_point.y);
    println!("wrapped_text: {}", wrapped_text.0);
}
integer_point: (4, -9)
float_point: (1.5, 3.25)
wrapped_text: payload

In Point<T>, both x and y share the single type parameter T. Initializing x and y with different concrete types fails at compile time because T is bound to the type of the first field (x):

// ❌ does not compile
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let mismatched = Point { x: 5, y: 4.0 };
}
error[E0308]: mismatched types
 --> src/main.rs:8:39
  |
8 |     let mismatched = Point { x: 5, y: 4.0 };
  |                                       ^^^ expected integer, found floating-point number

Memory Layout of Generic Structs

A generic struct is a type constructor: Point alone is not a concrete type, whereas Point<i32> and Point<f64> are distinct concrete types with their own stack sizes and alignments.

Point<u8> (2 bytes, align 1)      Point<u64> (16 bytes, align 8)
+------+------+                   +---------------+---------------+
| x:u8 | y:u8 |                   |   x: u64      |   y: u64      |
+------+------+                   +---------------+---------------+
0      1      2                   0               8              16

Unlike managed languages that box generic fields behind heap pointers, Rust embeds T directly inline inside the struct layout:

use std::mem::{align_of, size_of};

#[allow(dead_code)]
struct Pair<T> {
    first: T,
    second: T,
}

fn main() {
    println!(
        "Pair<u8>:  size = {} bytes, align = {} bytes",
        size_of::<Pair<u8>>(),
        align_of::<Pair<u8>>()
    );
    println!(
        "Pair<u64>: size = {} bytes, align = {} bytes",
        size_of::<Pair<u64>>(),
        align_of::<Pair<u64>>()
    );
    println!(
        "Pair<String>: size = {} bytes, align = {} bytes",
        size_of::<Pair<String>>(),
        align_of::<Pair<String>>()
    );
}
Pair<u8>:  size = 2 bytes, align = 1 bytes
Pair<u64>: size = 16 bytes, align = 8 bytes
Pair<String>: size = 48 bytes, align = 8 bytes

Conditional Trait Derivation on Generic Structs

When you place #[derive(Debug, Clone, PartialEq)] on a generic struct Container<T>, the compiler generates conditional implementations that apply only when T also implements the corresponding trait:

#[derive(Debug, Clone, PartialEq)]
struct BoxedMetric<T> {
    name: &'static str,
    reading: T,
}

#[derive(Debug)]
struct NonCloneable(i32);

fn main() {
    let m1 = BoxedMetric {
        name: "cpu_temp",
        reading: 65.4_f64,
    };
    let m2 = m1.clone();
    println!("Equal: {}", m1 == m2);

    // BoxedMetric<NonCloneable> still implements Debug, even though it cannot be cloned:
    let raw = BoxedMetric {
        name: "handle",
        reading: NonCloneable(9),
    };
    println!("Raw metric: {raw:?}");
}
Equal: true
Raw metric: BoxedMetric { name: "handle", reading: NonCloneable(9) }

Every Unused Type Parameter Is Rejected

Rust requires every type parameter declared on a struct to be used in at least one field. Declaring an unused type parameter produces error E0392:

// ❌ does not compile
struct TaggedId<T> {
    raw_id: u64,
}

fn main() {
    let _id = TaggedId::<String> { raw_id: 42 };
}
error[E0392]: type parameter `T` is never used
 --> src/main.rs:2:17
  |
2 | struct TaggedId<T> {
  |                 ^ unused type parameter
  |
  = help: consider removing `T`, referring to it in a field, or using a marker such as `PhantomData`

When a struct needs a compile-time type tag without storing a value of type T at runtime, include a zero-sized std::marker::PhantomData<T> field:

use std::marker::PhantomData;

#[derive(Debug)]
struct TaggedId<T> {
    raw_id: u64,
    _marker: PhantomData<T>,
}

struct User;
struct Order;

fn main() {
    let user_id: TaggedId<User> = TaggedId {
        raw_id: 101,
        _marker: PhantomData,
    };
    let order_id: TaggedId<Order> = TaggedId {
        raw_id: 101,
        _marker: PhantomData,
    };

    println!("user_id = {}, order_id = {}", user_id.raw_id, order_id.raw_id);
    println!("size of TaggedId<User> = {} bytes", std::mem::size_of::<TaggedId<User>>());
}
user_id = 101, order_id = 101
size of TaggedId<User> = 8 bytes