# Rust Syntax Generics

> 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".

- Skill: `impertio-studio/rust-syntax-generics` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/rust-syntax-generics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/rust-syntax-generics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/rust-syntax-generics

---


# 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.

```rust
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:

```rust
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:

```rust
impl<T> Point<T> {                  // T introduced here
    fn new(x: T, y: T) -> Self { Self { x, y } }
}

impl<T: std::ops::Add<Output = T> + Copy> Point<T> {  // bounded impl
    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:

```rust
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:

```rust
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 `+`:

```rust
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+):

```rust
fn sum<I>(iter: I) -> i32
where
    I: Iterator<Item = i32>,           // exact-type associated bound
{
    iter.sum()
}

fn print_items<I>(iter: I)
where
    I: Iterator<Item: Display>,        // bound on associated type, since 1.79
{
    for x in iter { println!("{x}"); }
}
```

---

## Monomorphization in practice

Every distinct concrete instantiation produces a distinct compiled function. Given:

```rust
fn id<T>(x: T) -> T { x }

fn main() {
    let a = id(1u32);     // emits id::<u32>
    let b = id(1u64);     // emits id::<u64>
    let c = id("hi");     // emits id::<&str>
}
```

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.

```rust
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

```rust
// Compile-time-sized buffer wrapper
struct Ring<T, const CAP: usize> {
    buf: [Option<T>; CAP],
    head: usize,
    len: usize,
}

// Array slicing helper
fn first_n<T, const N: usize>(arr: &[T]) -> Option<&[T; N]> {
    arr.first_chunk::<N>()    // since 1.77
}
```

---

## 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.

```rust
struct Vec<T, A: Allocator = Global> { /* ... */ }
//                       ^^^^^^^^ default

let v: Vec<i32> = Vec::new();          // A defaults to Global
let v: Vec<i32, MyAlloc> = ...;        // A explicit
```

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:

```rust
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:

```rust
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."

```rust
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:

```rust
use std::marker::PhantomData;

struct Tagged<T> {
    id: u64,
    _marker: PhantomData<T>,           // tells the compiler: T is significant
}
```

`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:

1. **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.
2. **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

- [`references/methods.md`](references/methods.md) : every generic-related syntax form with formal grammar
- [`references/examples.md`](references/examples.md) : working compilable examples for every pattern above
- [`references/anti-patterns.md`](references/anti-patterns.md) : six common mistakes with the WHY each one fails

---

## Authoritative sources

- The Rust Book Ch. 10.1 : https://doc.rust-lang.org/book/ch10-01-syntax.html
- Rust Reference, generic parameters : https://doc.rust-lang.org/reference/items/generics.html
- Rust Reference, type parameters : https://doc.rust-lang.org/reference/types/parameters.html
- Rust Reference, special types and traits (`Sized`, `?Sized`) : https://doc.rust-lang.org/reference/special-types-and-traits.html
- `std::marker::PhantomData` : https://doc.rust-lang.org/std/marker/struct.PhantomData.html
- Rust 1.65 release notes (GATs) : https://blog.rust-lang.org/2022/11/03/Rust-1.65.0/
- Rust 1.79 release notes (associated-type bounds) : https://blog.rust-lang.org/2024/06/13/Rust-1.79.0/
- Edition 2024, RPIT lifetime capture : https://doc.rust-lang.org/edition-guide/rust-2024/rpit-lifetime-capture.html

