Generics & Const Generics
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Generics let one definition
work for many types: Vec<T>, Option<T>, HashMap<K, V> and fn max<T: Ord> are all generic. Rust implements them
by monomorphization — generating specialised code for each concrete type — so generic code is as fast as
hand-written code. Constraints on what a type parameter can do are expressed with traits, covered in depth on
Traits.
Generic functions
A type parameter is declared in angle brackets after the function name. Inside the body you can only do what the
parameter’s bounds allow — with no bounds, a T can be moved, dropped and referenced, and nothing else:
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
let mut best = items.first()?;
for item in items {
if item > best {
best = item;
}
}
Some(best)
}
fn main() {
println!("{:?}", largest(&[1965, 1815, 1987])); // Some(1987)
println!("{:?}", largest(&["Dune", "Emma", "Beloved"])); // Some("Emma")
println!("{:?}", largest::<f64>(&[])); // None -- turbofish picks T
}
Source: The Book: Generic Data Types
Without the PartialOrd bound, item > best is error E0369
("binary operation > cannot be applied to type `&T`"), and the compiler suggests the missing bound. Rust checks
generic code once, against its bounds, rather than at each instantiation as C++ templates do; errors therefore point
at the definition, not at some distant call.
Generic structs, enums and methods
#[derive(Debug, Clone, PartialEq)]
struct Page<T> {
items: Vec<T>,
page: u32,
total: u64,
}
impl<T> Page<T> { // methods for every T
fn map<U>(self, f: impl FnMut(T) -> U) -> Page<U> {
Page { items: self.items.into_iter().map(f).collect(), page: self.page, total: self.total }
}
fn is_last(&self, per_page: u64) -> bool {
u64::from(self.page) * per_page >= self.total
}
}
impl Page<String> { // methods only for Page<String>
fn joined(&self) -> String {
self.items.join(", ")
}
}
#[derive(Debug)]
enum Lookup<T, E> { // like Result, with a third case
Found(T),
Missing,
Failed(E),
}
fn main() {
let page = Page { items: vec!["Dune", "Emma"], page: 1, total: 2 };
let owned = page.map(str::to_uppercase);
println!("{} last={}", owned.joined(), owned.is_last(20));
let l: Lookup<u16, String> = Lookup::Found(1965);
println!("{l:?} {:?}", Lookup::<u16, String>::Missing);
}
Source: Rust by Example: Implementation
impl<T> Page<T> declares T for the block; impl Page<String> without <T> targets one concrete type. Methods can
introduce their own extra parameters (map<U>).
Monomorphization and its cost
For each set of concrete types a generic item is used with, the compiler generates a separate copy: largest::<i32>,
largest::<&str> and so on. Each copy is optimised for its type, so there is no run-time cost — no boxing, no
dynamic dispatch. The costs are at build time and in binary size:
-
more machine code (code bloat) when a large generic function is used with many types;
-
longer compile times, because each copy goes through LLVM separately.
When that matters, use dynamic dispatch with a trait object (&dyn Trait), which compiles once (see
Trait Objects & dyn Compatibility), or the
inner function trick — a thin generic wrapper that converts its argument and calls a non-generic function, as the
standard library does for fs::read_to_string<P: AsRef<Path>>:
use std::path::Path;
pub fn count_lines<P: AsRef<Path>>(path: P) -> std::io::Result<usize> {
fn inner(path: &Path) -> std::io::Result<usize> { // compiled once
Ok(std::fs::read_to_string(path)?.lines().count())
}
inner(path.as_ref()) // tiny per-type wrapper
}
fn main() {
println!("{:?}", count_lines("/etc/hostname").is_ok() || count_lines(String::from("/nope")).is_err());
}
Bounds and where clauses
Bounds can be written inline (T: Display + Clone) or in a where clause after the signature. where is clearer when
there are several parameters or long bounds, and it is the only place for bounds that are not on a plain parameter
(such as Vec<T>: Debug or for<'a> &'a T: IntoIterator):
use std::collections::HashMap;
use std::fmt::Display;
use std::hash::Hash;
fn tally<I, K>(items: I) -> Vec<(K, usize)>
where
I: IntoIterator<Item = K>,
K: Hash + Eq + Ord + Clone,
{
let mut counts: HashMap<K, usize> = HashMap::new();
for item in items {
*counts.entry(item).or_default() += 1;
}
let mut out: Vec<_> = counts.into_iter().collect();
out.sort();
out
}
fn print_all<T>(items: &[T])
where
T: Display,
{
for i in items {
print!("{i} ");
}
println!();
}
fn main() {
println!("{:?}", tally(["sci-fi", "classic", "sci-fi"]));
print_all(&[1.5, 2.25]);
}
Source: Rust by Example: Where clauses
The turbofish
When the compiler cannot infer a type parameter — typically for collect, parse, into and Default::default — specify it with the turbofish ::<> on the function or method, or annotate the variable:
fn main() {
let years = "1965,1815".split(',').map(|s| s.parse::<u16>()).collect::<Result<Vec<_>, _>>();
let words: Vec<&str> = "Dune Emma".split(' ').collect(); // annotation instead of turbofish
let zero = u64::default();
let empty = Vec::<String>::new();
println!("{years:?} {words:?} {zero} {}", empty.len());
}
Source: std: Iterator::collect
Const generics
Const generics parameterise items over values — currently integers, bool and char — written
const N: usize. They make array lengths part of generic APIs without macros:
#[derive(Debug)]
struct RingLog<const N: usize> {
entries: [Option<&'static str>; N],
next: usize,
}
impl<const N: usize> RingLog<N> {
fn new() -> Self {
Self { entries: [None; N], next: 0 }
}
fn push(&mut self, entry: &'static str) {
self.entries[self.next % N] = Some(entry);
self.next += 1;
}
const CAPACITY: usize = N;
}
fn sum<const N: usize>(values: [u32; N]) -> u32 {
values.iter().sum()
}
fn main() {
let mut log = RingLog::<3>::new();
for e in ["lend", "return", "renew", "lend"] {
log.push(e);
}
println!("{:?} capacity={}", log.entries, RingLog::<3>::CAPACITY);
println!("{}", sum([1, 2, 3])); // N inferred as 3
let digits: [u8; _] = [9, 7, 8]; // `_` for an inferred const argument (1.89)
println!("{}", digits.len());
}
On stable, const parameters must be used as standalone values: expressions such as [T; N + 1] in a type
(generic_const_exprs) are still nightly-only. Since 1.89, _ can stand for an inferred const argument in types and
expressions such as [u8; _] or RingLog::<_>::new(). Arrays of any length implement the standard traits
(Debug, Default, PartialEq, IntoIterator) through const generics.
Default type parameters
A type parameter can have a default, used when the caller does not specify it. The standard library uses this for
HashMap<K, V, S = RandomState> and for operator traits (trait Add<Rhs = Self>):
use std::collections::hash_map::RandomState;
use std::collections::HashMap;
struct Index<K, V, S = RandomState> {
map: HashMap<K, V, S>,
}
fn main() {
let idx: Index<&str, u16> = Index { map: HashMap::new() }; // S defaults to RandomState
println!("{}", idx.map.len());
}
PhantomData and marker types
A generic struct must use each type parameter in a field; an unused T is error
E0392. When a type logically relates to T without storing one — an ID for a T, a handle to C memory holding Ts — add a zero-sized PhantomData<T> field. It tells the compiler how
the type relates to T for variance, auto traits (Send/Sync) and drop checking:
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Id<T> {
raw: u64,
_type: PhantomData<fn() -> T>, // `fn() -> T`: covariant, and Send + Sync whatever T is
}
impl<T> Id<T> {
fn new(raw: u64) -> Self {
Self { raw, _type: PhantomData }
}
}
struct Book;
struct Member;
fn lend(book: Id<Book>, member: Id<Member>) {
println!("lend book {} to member {}", book.raw, member.raw);
}
fn main() {
let b = Id::<Book>::new(7);
let m = Id::<Member>::new(42);
lend(b, m);
// lend(m, b); // would not compile: expected `Id<Book>`, found `Id<Member>` (E0308)
println!("{}", size_of::<Id<Book>>()); // 8 -- PhantomData is zero-sized
}
Source: The Rustonomicon: PhantomData
The choice of PhantomData<T>, PhantomData<fn() -> T> or PhantomData<*const T> matters: PhantomData<T> says
"owns a T`" (affects drop check and makes `Id<T> Send only if T is), the function-pointer form avoids that, and
the raw-pointer form makes the type neither Send nor Sync. The deriving gotcha: #[derive(Clone, Copy, ...)] on a
generic struct adds T: Clone bounds, even though PhantomData<T> is always Copy; above it works because Book and
Member are used only as type markers in Id<...>, but for real code write the impls by hand or use a crate such as
derive-where.
Typestate: checking state transitions at compile time
Marker types and generics combine into the typestate pattern: encode an object’s state in its type, so that calling a method in the wrong state is a compile error rather than a run-time check. The shelf import job must be configured, then validated, then run:
use std::marker::PhantomData;
struct Configured;
struct Validated;
struct ImportJob<State> {
path: String,
rows: usize,
_state: PhantomData<State>,
}
impl ImportJob<Configured> {
fn new(path: &str) -> Self {
ImportJob { path: path.to_owned(), rows: 0, _state: PhantomData }
}
fn validate(self) -> Result<ImportJob<Validated>, String> {
if !self.path.ends_with(".json") {
return Err(format!("{} is not a JSON file", self.path));
}
Ok(ImportJob { path: self.path, rows: 3, _state: PhantomData })
}
}
impl ImportJob<Validated> {
fn run(self) -> usize {
println!("importing {} rows from {}", self.rows, self.path);
self.rows
}
}
fn main() {
let job = ImportJob::new("books.json");
// job.run(); // would not compile: no method `run` for ImportJob<Configured> (E0599)
match job.validate() {
Ok(valid) => println!("{} imported", valid.run()),
Err(e) => eprintln!("{e}"),
}
}
Typestate works best for short, linear protocols (builders, connections, file handles). For states that are only known
at run time — loaded from a database, chosen by user input — an enum (see the LoanState example on
Enums & Pattern Matching) is simpler.
References
-
Rust by Example: Generics (functions, implementation, bounds, where clauses, new type idiom, phantom types)