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