| name | rust-syntax-generics |
| description | Use when the user writes generic functions / types / impl blocks, needs trait bounds, where clauses, const generics, or wonders about monomorphization vs dyn dispatch trade-offs. Prevents over-constraining with `'static`, mixing up trait-bound syntax, or assuming generic Rust code emits runtime polymorphism (it doesn't, it's monomorphized). Covers: type parameters `<T>`, trait bounds `T: Trait`, multiple bounds with `+`, `where` clauses, monomorphization implications (code-size, compile-time), const generics (`N: usize`, where they can/cannot appear), generic lifetime parameters mixed with type parameters, default generic types (`<T = Default>`). Keywords: generic, "type parameter", "<T>", "trait bound", "where clause", "T: Trait", monomorphization, "code bloat", "const generic", "N: usize", "generic function", "generic struct", "generic impl", "default generic type", PhantomData generic, "compile time generics", "expanded at compile time", "generic over".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
rust-syntax-generics
Generic programming in Rust: type parameters <T>, trait bounds, where clauses, const generics <const N: usize>, default generic types, and how monomorphization shapes the trade-off between zero-cost abstraction and binary size. Generics are a compile-time feature: every concrete instantiation produces its own machine code. Runtime polymorphism in Rust requires dyn Trait (see [[rust-syntax-trait-objects]]).
Cross-references: [[rust-syntax-traits]] (trait declarations, blanket impls, orphan rule) [[rust-syntax-trait-objects]] (dyn Trait runtime alternative) [[rust-syntax-lifetimes]] (lifetime parameters mixed with type parameters) [[rust-syntax-gats]] (Generic Associated Types, built on this skill) [[rust-core-language-versions]] (which generic features stabilized when).
When to use this skill
- User writes
fn foo<T>(x: T), struct Foo<T>, enum Result<T, E>, or impl<T> Foo<T>
- User needs to constrain a type parameter with a trait:
T: Display + Clone
- User wonders whether to use a
where clause vs inline bounds
- User asks "is this expanded at compile time" / "what's the runtime cost of generics"
- User defines an array-size-generic API:
fn process<const N: usize>(buf: [u8; N])
- User encounters confusing error:
T does not live long enough and over-applies 'static
- User asks about default generic types (
Vec<T, A = Global>, HashMap<K, V, S = RandomState>)
- User mixes generic lifetime and type parameters:
struct Wrapper<'a, T>
For trait declarations themselves (defining trait Foo {}) see [[rust-syntax-traits]]. For runtime polymorphism (heterogeneous collections) see [[rust-syntax-trait-objects]].
The one rule that explains everything
Generics in Rust are monomorphized at compile time. The compiler generates one distinct copy of the generic item for each unique set of concrete type arguments it is instantiated with. From the research: "Rust resolves all type parameters at compile time. Every concrete instantiation of a generic function produces a distinct symbol in the binary. This delivers zero-cost abstraction at the price of compile time and binary size." Generic Rust has zero runtime dispatch cost but non-zero binary cost.
If you want runtime polymorphism (one function pointer, multiple types behind it), you must explicitly opt into dyn Trait (see [[rust-syntax-trait-objects]]).
Quick reference table
| Construct | Syntax | Where |
|---|
| Generic function | fn foo<T>(x: T) -> T | item declaration |
| Generic struct | struct Wrap<T> { value: T } | item declaration |
| Generic enum | enum Either<L, R> { Left(L), Right(R) } | item declaration |
| Generic impl block | impl<T> Wrap<T> { ... } | impl declaration |
| Generic impl with bound | impl<T: Display> Wrap<T> { ... } | impl header |
| Trait bound (inline) | fn foo<T: Display>(x: T) | parameter list |
| Multiple bounds | T: Display + Clone + Send | bound position |
| Where clause | fn foo<T>(x: T) where T: Display + Clone | after signature |
| Const generic | fn arr<const N: usize>(a: [u8; N]) | parameter list |
| Default generic type | struct Vec<T, A: Allocator = Global> | item declaration |
| Lifetime + type | struct Ref<'a, T> { r: &'a T } | item declaration |
| HRTB | for<'a> Fn(&'a T) | bound position |
Sized opt-out | fn foo<T: ?Sized>(x: &T) | bound position |
Type parameters
A type parameter is a placeholder filled in by the compiler at every call site. Type parameters use single uppercase letters by convention (T, U, K, V, E) but any identifier in UpperCamelCase is legal.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
ALWAYS list every type parameter in <...> after the item name. The bound T: PartialOrd is required because > calls PartialOrd::gt, and without the bound gt would not be callable.
Generic types follow the same rule:
struct Point<T> { x: T, y: T }
enum Either<L, R> { Left(L), Right(R) }
For an impl block, the generic parameters must be introduced in the impl header, even if they are already in the type name:
impl<T> Point<T> {
fn new(x: T, y: T) -> Self { Self { x, y } }
}
impl<T: std::ops::Add<Output = T> + Copy> Point<T> {
fn sum(&self) -> T { self.x + self.y }
}
NEVER write impl Point<T> without impl<T>: it would attempt to impl on a concrete type named T (or fail with E0412).
Trait bounds
Trait bounds restrict which concrete types may be substituted for a type parameter. Without a bound, the only operations callable on T are those defined for all types (move, drop, mem::size_of).
Inline vs where clause
Both syntaxes are equivalent. Inline bounds are concise for one or two simple bounds:
fn print_pair<T: Display, U: Display>(t: T, u: U) {
println!("{t} {u}");
}
A where clause is preferred for three or more bounds, multi-parameter bounds, or associated-type bounds:
fn process<I, T>(iter: I)
where
I: IntoIterator<Item = T>,
T: Display + Clone + Send + 'static,
{
for item in iter { println!("{item}"); }
}
ALWAYS use a where clause when bounds reference associated types (I: Iterator<Item = T>) or when bounds span more than ~60 characters; readability is the deciding factor. NEVER write the same bound twice in inline AND where form for the same parameter; pick one.
Multiple bounds with +
Combine bounds with +:
fn announce<T: Display + Clone + Send + 'static>(value: T) { }
The + joiner works in inline and where positions identically.
Associated-type bounds
Bounds on associated types use the Trait<Item = X> form (introduced 1.0) or the newer Trait<Item: SomeBound> form (1.79+):
fn sum<I>(iter: I) -> i32
where
I: Iterator<Item = i32>,
{
iter.sum()
}
fn print_items<I>(iter: I)
where
I: Iterator<Item: Display>,
{
for x in iter { println!("{x}"); }
}
Monomorphization in practice
Every distinct concrete instantiation produces a distinct compiled function. Given:
fn id<T>(x: T) -> T { x }
fn main() {
let a = id(1u32);
let b = id(1u64);
let c = id("hi");
}
The binary contains three distinct copies of id. Implications:
- Zero runtime cost: each call is a direct call to a specialized function. Inlining works as usual.
- Larger binary: every type combination multiplies code size. A 50-line generic function instantiated for 20 types adds ~1000 lines of generated assembly.
- Longer compile times: codegen runs once per instantiation.
- No vtable, no fat pointer: a
T value has the exact size and layout of its concrete type.
ALWAYS prefer generics over dyn Trait for hot paths where dispatch cost matters. Choose dyn Trait when (1) the type set is open-ended at runtime, (2) heterogeneous collections are needed (Vec<Box<dyn Drawable>>), or (3) binary size dominates.
Const generics
Const generics are type parameters that hold values, not types. Stable for primitive integer types, char, and bool since Rust 1.51 (Min Const Generics). Generic const expressions remain unstable.
fn fill<const N: usize>(value: u8) -> [u8; N] {
[value; N]
}
let buf: [u8; 16] = fill::<16>(0);
struct Matrix<const R: usize, const C: usize> {
data: [[f64; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
fn new() -> Self { Self { data: [[0.0; C]; R] } }
}
What stable const generics CANNOT do
- Generic const expressions (
feature(generic_const_exprs)): you cannot write [u8; N + 1] or [u8; { N * 2 }] in stable Rust. NEVER use these on stable; the feature is unstable as of Rust 1.87.
- Const generic types beyond integers/
bool/char: structs, floats, and user-defined types are not allowed as const generic parameters on stable.
- Defaults that depend on other const params:
const M: usize = N is rejected.
Stable const-generic patterns
struct Ring<T, const CAP: usize> {
buf: [Option<T>; CAP],
head: usize,
len: usize,
}
fn first_n<T, const N: usize>(arr: &[T]) -> Option<&[T; N]> {
arr.first_chunk::<N>()
}
Default generic types
A generic parameter can have a default, used when callers omit the argument. Defaults apply to the declaration site, not the call site of methods.
struct Vec<T, A: Allocator = Global> { }
let v: Vec<i32> = Vec::new();
let v: Vec<i32, MyAlloc> = ...;
NEVER rely on default generic types for function parameters; only struct/enum/trait declarations honor them. A fn foo<T = i32>() is a syntax error.
Generic lifetime parameters
Lifetime parameters appear in the same <...> as type parameters, ALWAYS listed first by convention:
struct Borrowed<'a, T: 'a> {
inner: &'a T,
}
impl<'a, T> Borrowed<'a, T> {
fn new(r: &'a T) -> Self { Self { inner: r } }
}
The bound T: 'a reads "any references inside T outlive 'a". For most owned types (String, Vec<u8>, i32), this bound is automatically satisfied because they contain no references. ALWAYS write T: 'a (or rely on the implicit T: 'a that the compiler inserts on generic types holding &'a T) instead of jumping to T: 'static. See [[rust-syntax-lifetimes]] for the full story.
Higher-Ranked Trait Bounds (HRTB)
When a closure must accept references of any lifetime:
fn apply<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
let s = String::from("hi");
let _ = f(&s);
}
The for<'a> quantifies over all lifetimes; without it, the type inference would pin a single concrete 'a.
Sized and ?Sized
By default, every type parameter has an implicit T: Sized bound. From the Reference: "Type parameters (except Self in traits) are Sized by default, as are associated types. These implicit Sized bounds may be relaxed by using the special ?Sized bound."
fn print_dst<T: ?Sized + Display>(value: &T) {
println!("{value}");
}
NEVER write the redundant explicit T: Sized bound; the compiler already inserts it. ONLY use T: ?Sized when you genuinely need to accept dynamically-sized types like str, [T], or dyn Trait. Restrictions: ?Sized types can only be used behind a pointer (&T, &mut T, Box<T>, Rc<T>, etc.).
PhantomData and generic markers
When a generic type does not actually hold a T, the compiler refuses to accept the parameter unless PhantomData<T> declares the intent:
use std::marker::PhantomData;
struct Tagged<T> {
id: u64,
_marker: PhantomData<T>,
}
PhantomData<T> has zero size (verified: size_of::<PhantomData<T>>() == 0). Use it to:
- Express that a type parameter influences variance, drop-check, or trait selection.
- Carry a phantom lifetime:
PhantomData<&'a ()>.
- Mark thread-safety:
PhantomData<*mut ()> makes a type !Send + !Sync.
See the variance table in [[rust-syntax-lifetimes]] for how PhantomData<T> is covariant in T.
Decision tree: generics vs trait objects
Need polymorphism?
├── At compile time (type set known) ──> generics (this skill)
│ ├── Performance-critical hot path ─> generics, monomorphized
│ └── Many type combinations? ──────> watch binary size, may switch to dyn
└── At runtime (type set open / heterogeneous) ──> dyn Trait (see rust-syntax-trait-objects)
├── Heterogeneous collection ─────> Vec<Box<dyn Trait>>
└── Plugin / unknown-at-compile types ──> dyn Trait
Edition 2024 interactions
Generics interact with edition 2024 in two places:
- RPIT lifetime capture: in edition 2024,
-> impl Trait implicitly captures all in-scope generic parameters, including lifetimes. Pre-2024 code that relied on impl Trait not capturing a lifetime needs use<> syntax to preserve old behavior. See [[rust-syntax-traits]] for impl Trait semantics and cargo fix --edition for automatic migration.
- Never-type fallback:
!-to-any coercions now fall back to ! (was ()). This rarely affects generic code directly but can change which generic impls are selected when ? returns Err(_) -> !.
Reference files
Authoritative sources