Skip to content
Learn Rust
Theme

Generics

Abstractions · Lesson 33

A generic function or type is written once with a placeholder, T, and the compiler fills it in for each type you use. A bound such as T: PartialOrd says what T must be able to do, and the body may use only that.

Coming from Gofn largest<T: PartialOrd> is func largest[T cmp.Ordered], with the type parameter in angle brackets. The bound is a trait, so any type that implements PartialOrd, including your own structs, qualifies, not just the built-in ordered types.

Coming from Kotlin<T: PartialOrd> reads like <T : Comparable<T>>. Rust compiles a separate copy of the function for each type you use, so there is no type erasure and no reified keyword: TypeId::of::<T>() identifies a 'static type, and trait functions such as T::default() cover the rest.

Coming from Pythonfn largest<T: PartialOrd> is the 3.12 syntax def largest[T: Comparable], but the compiler enforces the bound. You can't use > on T unless the bound says it can. Python does not check hints when the program runs; a type checker such as mypy can, but only if you run one.

Coming from TypeScript<T: PartialOrd> is like <T extends ...>, but the bound is a trait (something the type can do), not a list of allowed types. Any type with PartialOrd, including your own struct with #[derive(PartialOrd)], can be passed in.

Coming from Javafn largest<T: PartialOrd> is <T extends Comparable<T>>, and you can use < and > directly. Rust generics are not erased: the compiler makes a separate copy for each type used, so T works with plain numbers without boxing.

Coming from C#<T: PartialOrd> is where T : IComparable<T>, and you can use < and > instead of CompareTo. Like C#, Rust does not erase T, but it gets there differently: the compiler makes a separate copy of the function for each type, so no T exists at run time (C# keeps it, and typeof(T) works).

Coming from CC has no generics, so you'd write largest_int and largest_str by hand or paste text with a macro. Rust writes those copies for you and type-checks them.

Coming from C++Generics are templates with a concept, T: PartialOrd playing the role of std::totally_ordered T, and each type gets its own compiled copy. The difference is that Rust checks the function body against the bound once, so a missing bound is reported at the definition. A concept checks the call, not the body: a + it never promised still fails per type, inside the body.

The quick comparison

Rustsrc/main.rsRun ↗
// T can be any type that can be compared with < and >
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
best
}
fn main() {
println!("{}", largest(&[3, 9, 2]));
println!("{}", largest(&["kiwi", "apple", "pear"]));
}
$ cargo run
9
pear
package main
import (
"cmp"
"fmt"
)
func largest[T cmp.Ordered](items []T) T {
best := items[0]
for _, item := range items {
if item > best {
best = item
}
}
return best
}
func main() {
fmt.Println(largest([]int{3, 9, 2}))
fmt.Println(largest([]string{"kiwi", "apple", "pear"}))
}
$ go run main.go
9
pear
fun <T : Comparable<T>> largest(items: List<T>): T {
var best = items[0]
for (item in items) {
if (item > best) {
best = item
}
}
return best
}
fun main() {
println(largest(listOf(3, 9, 2)))
println(largest(listOf("kiwi", "apple", "pear")))
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
9
pear
from typing import Protocol, Self
class Comparable(Protocol):
def __gt__(self, other: Self, /) -> bool: ...
def largest[T: Comparable](items: list[T]) -> T:
best = items[0]
for item in items:
if item > best:
best = item
return best
print(largest([3, 9, 2]))
print(largest(["kiwi", "apple", "pear"]))
$ python3 main.py
9
pear
// T can be a number or a string: both can be compared with < and >
function largest<T extends number | string>(items: readonly T[]): T {
let best = items[0];
for (const item of items) {
if (item > best) {
best = item;
}
}
return best;
}
console.log(largest([3, 9, 2]));
console.log(largest(["kiwi", "apple", "pear"]));
$ tsc --strict --module nodenext --types node main.ts && node main.js
9
pear
import java.util.List;
public class Main {
// T can be any type that can be compared to itself
static <T extends Comparable<T>> T largest(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
public static void main(String[] args) {
System.out.println(largest(List.of(3, 9, 2)));
System.out.println(largest(List.of("kiwi", "apple", "pear")));
}
}
$ javac Main.java && java Main
9
pear
Console.WriteLine(Largest([3, 9, 2]));
Console.WriteLine(Largest(["kiwi", "apple", "pear"]));
// T can be any type that can compare itself to another T
static T Largest<T>(T[] items) where T : IComparable<T>
{
var best = items[0];
foreach (var item in items)
{
if (item.CompareTo(best) > 0)
{
best = item;
}
}
return best;
}
$ dotnet run
9
pear
#include <stdio.h>
#include <string.h>
// C has no generics: you write one version per type.
const int *largest_int(const int *items, size_t len) {
const int *best = &items[0];
for (size_t i = 0; i < len; i++) {
if (items[i] > *best) best = &items[i];
}
return best;
}
const char *const *largest_str(const char *const *items, size_t len) {
const char *const *best = &items[0];
for (size_t i = 0; i < len; i++) {
if (strcmp(items[i], *best) > 0) best = &items[i];
}
return best;
}
int main(void) {
int nums[] = {3, 9, 2};
const char *fruits[] = {"kiwi", "apple", "pear"};
printf("%d\n", *largest_int(nums, 3));
printf("%s\n", *largest_str(fruits, 3));
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
9
pear
#include <concepts>
#include <iostream>
#include <string_view>
#include <vector>
// T can be any type that can be compared with < and >
template <std::totally_ordered T>
const T& largest(const std::vector<T>& items) {
const T* best = &items[0];
for (const T& item : items) {
if (item > *best) {
best = &item;
}
}
return *best;
}
int main() {
std::vector<int> nums{3, 9, 2};
std::vector<std::string_view> fruit{"kiwi", "apple", "pear"};
std::cout << largest(nums) << "\n";
std::cout << largest(fruit) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
9
pear

What's different

  • T: PartialOrd means "any type T whose values can be compared with < and >". Without it, the compiler won't let you compare two T values.
  • This returns &T, a reference to the biggest item inside items, so nothing is copied.
  • Several bounds: T: PartialOrd + Display. Long lists go in a where clause after the signature.
  • Rust makes you state the constraint up front. largest<T> with no bound is a compile error, because the compiler checks a generic function on its own, before anyone calls it.
Gotcha

Adding two Ts and returning a T

Looks fine: in Go, Python, C#, C and C++ it runs fine. Rust won't compile it.

Works in Go. In Rust it's just a rule.Kotlin refuses this too. Same idea, different words.Works in Python. In Rust it's just a rule.TypeScript refuses this too. Same idea, different words.Java refuses this too. Same idea, different words.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
use std::ops::Add;
fn add<T: Add>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("{}", add(2, 3));
println!("{}", add(1.5, 2.25));
}
package main
import "fmt"
// T is one of these types, and `+` on a T gives a T.
func add[T int | float64](a, b T) T {
return a + b
}
func main() {
fmt.Println(add(2, 3))
fmt.Println(add(1.5, 2.25))
}
$ go run main.go✓ runs
5
3.75
Kotlin✗ refuses too

Kotlin refuses too. Number has no plus, so a + b on a T : Number finds nothing to call.

// Number has no `plus`, so there is no way to say "any type with +".
fun <T : Number> add(a: T, b: T): T = a + b
fun main() {
println(add(2, 3))
println(add(1.5, 2.25))
}
Main.kt:2:41: error: none of the following candidates is applicable:

fun BigDecimal.plus(other: BigDecimal): BigDecimal:
  Candidate 'fun BigDecimal.plus(other: BigDecimal): BigDecimal' is inapplicable because of a receiver type mismatch.

fun BigInteger.plus(other: BigInteger): BigInteger:
  Candidate 'fun BigInteger.plus(other: BigInteger): BigInteger' is inapplicable because of a receiver type mismatch.

fun String?.plus(other: Any?): String:
  Candidate 'fun String?.plus(other: Any?): String' is inapplicable because of a receiver type mismatch.

fun <T> Array<T>.plus(element: T): Array<T>:
  Candidate 'fun <T> Array<T>.plus(element: T): Array<T>' is inapplicable because of a receiver type mismatch.

fun ByteArray.plus(element: Byte): ByteArray:
  Candidate 'fun ByteArray.plus(element: Byte): ByteArray' is inapplicable because of a receiver type mismatch.

fun ShortArray.plus(element: Short): ShortArray:
  Candidate 'fun ShortArray.plus(element: Short): ShortArray' is inapplicable because of a receiver type mismatch.

fun IntArray.plus(element: Int): IntArray:
  Candidate 'fun IntArray.plus(element: Int): IntArray' is inapplicable because of a receiver type mismatch.

fun LongArray.plus(element: Long): LongArray:
  Candidate 'fun LongArray.plus(element: Long): LongArray' is inapplicable because of a receiver type mismatch.

fun FloatArray.plus(element: Float): FloatArray:
  Candidate 'fun FloatArray.plus(element: Float): FloatArray' is inapplicable because of a receiver type mismatch.

fun DoubleArray.plus(element: Double): DoubleArray:
  Candidate 'fun DoubleArray.plus(element: Double): DoubleArray' is inapplicable because of a receiver type mismatch.

fun BooleanArray.plus(element: Boolean): BooleanArray:
  Candidate 'fun BooleanArray.plus(element: Boolean): BooleanArray' is inapplicable because of a receiver type mismatch.

fun CharArray.plus(element: Char): CharArray:
  Candidate 'fun CharArray.plus(element: Char): CharArray' is inapplicable because of a receiver type mismatch.

fun <T> Array<T>.plus(elements: Collection<T>): Array<T>:
  Candidate 'fun <T> Array<T>.plus(elements: Collection<T>): Array<T>' is inapplicable because of a receiver type mismatch.

fun ByteArray.plus(elements: Collection<Byte>): ByteArray:
  Candidate 'fun ByteArray.plus(elements: Collection<Byte>): ByteArray' is inapplicable because of a receiver type mismatch.

fun ShortArray.plus(elements: Collection<Short>): ShortArray:
  Candidate 'fun ShortArray.plus(elements: Collection<Short>): ShortArray' is inapplicable because of a receiver type mismatch.

fun IntArray.plus(elements: Collection<Int>): IntArray:
  Candidate 'fun IntArray.plus(elements: Collection<Int>): IntArray' is inapplicable because of a receiver type mismatch.

fun LongArray.plus(elements: Collection<Long>): LongArray:
  Candidate 'fun LongArray.plus(elements: Collection<Long>): LongArray' is inapplicable because of a receiver type mismatch.

fun FloatArray.plus(elements: Collection<Float>): FloatArray:
  Candidate 'fun FloatArray.plus(elements: Collection<Float>): FloatArray' is inapplicable because of a receiver type mismatch.

fun DoubleArray.plus(elements: Collection<Double>): DoubleArray:
  Candidate 'fun DoubleArray.plus(elements: Collection<Double>): DoubleArray' is inapplicable because of a receiver type mismatch.

fun BooleanArray.plus(elements: Collection<Boolean>): BooleanArray:
  Candidate 'fun BooleanArray.plus(elements: Collection<Boolean>): BooleanArray' is inapplicable because of a receiver type mismatch.

fun CharArray.plus(elements: Collection<Char>): CharArray:
  Candidate 'fun CharArray.plus(elements: Collection<Char>): CharArray' is inapplicable because of a receiver type mismatch.

fun <T> Array<T>.plus(elements: Array<out T>): Array<T>:
  Candidate 'fun <T> Array<T>.plus(elements: Array<out T>): Array<T>' is inapplicable because of a receiver type mismatch.

fun ByteArray.plus(elements: ByteArray): ByteArray:
  Candidate 'fun ByteArray.plus(elements: ByteArray): ByteArray' is inapplicable because of a receiver type mismatch.

fun ShortArray.plus(elements: ShortArray): ShortArray:
  Candidate 'fun ShortArray.plus(elements: ShortArray): ShortArray' is inapplicable because of a receiver type mismatch.

fun IntArray.plus(elements: IntArray): IntArray:
  Candidate 'fun IntArray.plus(elements: IntArray): IntArray' is inapplicable because of a receiver type mismatch.

fun LongArray.plus(elements: LongArray): LongArray:
  Candidate 'fun LongArray.plus(elements: LongArray): LongArray' is inapplicable because of a receiver type mismatch.

fun FloatArray.plus(elements: FloatArray): FloatArray:
  Candidate 'fun FloatArray.plus(elements: FloatArray): FloatArray' is inapplicable because of a receiver type mismatch.

fun DoubleArray.plus(elements: DoubleArray): DoubleArray:
  Candidate 'fun DoubleArray.plus(elements: DoubleArray): DoubleArray' is inapplicable because of a receiver type mismatch.

fun BooleanArray.plus(elements: BooleanArray): BooleanArray:
  Candidate 'fun BooleanArray.plus(elements: BooleanArray): BooleanArray' is inapplicable because of a receiver type mismatch.

fun CharArray.plus(elements: CharArray): CharArray:
  Candidate 'fun CharArray.plus(elements: CharArray): CharArray' is inapplicable because of a receiver type mismatch.

fun <T> Iterable<T>.plus(element: T): List<T>:
  Candidate 'fun <T> Iterable<T>.plus(element: T): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Collection<T>.plus(element: T): List<T>:
  Candidate 'fun <T> Collection<T>.plus(element: T): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Iterable<T>.plus(elements: Array<out T>): List<T>:
  Candidate 'fun <T> Iterable<T>.plus(elements: Array<out T>): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Collection<T>.plus(elements: Array<out T>): List<T>:
  Candidate 'fun <T> Collection<T>.plus(elements: Array<out T>): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Iterable<T>.plus(elements: Iterable<T>): List<T>:
  Candidate 'fun <T> Iterable<T>.plus(elements: Iterable<T>): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Collection<T>.plus(elements: Iterable<T>): List<T>:
  Candidate 'fun <T> Collection<T>.plus(elements: Iterable<T>): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Iterable<T>.plus(elements: Sequence<T>): List<T>:
  Candidate 'fun <T> Iterable<T>.plus(elements: Sequence<T>): List<T>' is inapplicable because of a receiver type mismatch.

fun <T> Collection<T>.plus(elements: Sequence<T>): List<T>:
  Candidate 'fun <T> Collection<T>.plus(elements: Sequence<T>): List<T>' is inapplicable because of a receiver type mismatch.

fun <K, V> Map<out K, V>.plus(pair: Pair<K, V>): Map<K, V>:
  Candidate 'fun <K, V> Map<out K, V>.plus(pair: Pair<K, V>): Map<K, V>' is inapplicable because of a receiver type mismatch.

fun <K, V> Map<out K, V>.plus(pairs: Iterable<Pair<K, V>>): Map<K, V>:
  Candidate 'fun <K, V> Map<out K, V>.plus(pairs: Iterable<Pair<K, V>>): Map<K, V>' is inapplicable because of a receiver type mismatch.

fun <K, V> Map<out K, V>.plus(pairs: Array<out Pair<K, V>>): Map<K, V>:
  Candidate 'fun <K, V> Map<out K, V>.plus(pairs: Array<out Pair<K, V>>): Map<K, V>' is inapplicable because of a receiver type mismatch.

fun <K, V> Map<out K, V>.plus(pairs: Sequence<Pair<K, V>>): Map<K, V>:
  Candidate 'fun <K, V> Map<out K, V>.plus(pairs: Sequence<Pair<K, V>>): Map<K, V>' is inapplicable because of a receiver type mismatch.

fun <K, V> Map<out K, V>.plus(map: Map<out K, V>): Map<K, V>:
  Candidate 'fun <K, V> Map<out K, V>.plus(map: Map<out K, V>): Map<K, V>' is inapplicable because of a receiver type mismatch.

fun <T> Set<T>.plus(element: T): Set<T>:
  Candidate 'fun <T> Set<T>.plus(element: T): Set<T>' is inapplicable because of a receiver type mismatch.

fun <T> Set<T>.plus(elements: Array<out T>): Set<T>:
  Candidate 'fun <T> Set<T>.plus(elements: Array<out T>): Set<T>' is inapplicable because of a receiver type mismatch.

fun <T> Set<T>.plus(elements: Iterable<T>): Set<T>:
  Candidate 'fun <T> Set<T>.plus(elements: Iterable<T>): Set<T>' is inapplicable because of a receiver type mismatch.

fun <T> Set<T>.plus(elements: Sequence<T>): Set<T>:
  Candidate 'fun <T> Set<T>.plus(elements: Sequence<T>): Set<T>' is inapplicable because of a receiver type mismatch.

fun UIntArray.plus(element: UInt): UIntArray:
  Candidate 'fun UIntArray.plus(element: UInt): UIntArray' is inapplicable because of a receiver type mismatch.

fun ULongArray.plus(element: ULong): ULongArray:
  Candidate 'fun ULongArray.plus(element: ULong): ULongArray' is inapplicable because of a receiver type mismatch.

fun UByteArray.plus(element: UByte): UByteArray:
  Candidate 'fun UByteArray.plus(element: UByte): UByteArray' is inapplicable because of a receiver type mismatch.

fun UShortArray.plus(element: UShort): UShortArray:
  Candidate 'fun UShortArray.plus(element: UShort): UShortArray' is inapplicable because of a receiver type mismatch.

fun UIntArray.plus(elements: Collection<UInt>): UIntArray:
  Candidate 'fun UIntArray.plus(elements: Collection<UInt>): UIntArray' is inapplicable because of a receiver type mismatch.

fun ULongArray.plus(elements: Collection<ULong>): ULongArray:
  Candidate 'fun ULongArray.plus(elements: Collection<ULong>): ULongArray' is inapplicable because of a receiver type mismatch.

fun UByteArray.plus(elements: Collection<UByte>): UByteArray:
  Candidate 'fun UByteArray.plus(elements: Collection<UByte>): UByteArray' is inapplicable because of a receiver type mismatch.

fun UShortArray.plus(elements: Collection<UShort>): UShortArray:
  Candidate 'fun UShortArray.plus(elements: Collection<UShort>): UShortArray' is inapplicable because of a receiver type mismatch.

fun UIntArray.plus(elements: UIntArray): UIntArray:
  Candidate 'fun UIntArray.plus(elements: UIntArray): UIntArray' is inapplicable because of a receiver type mismatch.

fun ULongArray.plus(elements: ULongArray): ULongArray:
  Candidate 'fun ULongArray.plus(elements: ULongArray): ULongArray' is inapplicable because of a receiver type mismatch.

fun UByteArray.plus(elements: UByteArray): UByteArray:
  Candidate 'fun UByteArray.plus(elements: UByteArray): UByteArray' is inapplicable because of a receiver type mismatch.

fun UShortArray.plus(elements: UShortArray): UShortArray:
  Candidate 'fun UShortArray.plus(elements: UShortArray): UShortArray' is inapplicable because of a receiver type mismatch.

fun <T> Sequence<T>.plus(element: T): Sequence<T>:
  Candidate 'fun <T> Sequence<T>.plus(element: T): Sequence<T>' is inapplicable because of a receiver type mismatch.

fun <T> Sequence<T>.plus(elements: Array<out T>): Sequence<T>:
  Candidate 'fun <T> Sequence<T>.plus(elements: Array<out T>): Sequence<T>' is inapplicable because of a receiver type mismatch.

fun <T> Sequence<T>.plus(elements: Iterable<T>): Sequence<T>:
  Candidate 'fun <T> Sequence<T>.plus(elements: Iterable<T>): Sequence<T>' is inapplicable because of a receiver type mismatch.

fun <T> Sequence<T>.plus(elements: Sequence<T>): Sequence<T>:
  Candidate 'fun <T> Sequence<T>.plus(elements: Sequence<T>): Sequence<T>' is inapplicable because of a receiver type mismatch.

fun Char.plus(other: String): String:
  Candidate 'fun Char.plus(other: String): String' is inapplicable because of a receiver type mismatch.
fun <T : Number> add(a: T, b: T): T = a + b
                                        ^
def add[T](a: T, b: T) -> T:
return a + b
print(add(2, 3))
print(add(1.5, 2.25))
$ python3 main.py✓ runs
5
3.75
TypeScript✗ refuses too

TypeScript refuses too, in terms close to Rust's: the sum is a number, and T could be a narrower type, so it cannot be returned as a T.

function add<T extends number>(a: T, b: T): T {
return a + b;
}
console.log(add(2, 3));
console.log(add(1.5, 2.25));
main.ts(2,3): error TS2322: Type 'number' is not assignable to type 'T'.
  'number' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'number'.
Java✗ refuses too

Java refuses too (bad operand types for binary operator '+'). Number has no +, and a type variable cannot supply one.

public class Main {
// Number has no `+`: Java's operators work only on the primitive types.
static <T extends Number> T add(T a, T b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(2, 3));
System.out.println(add(1.5, 2.25));
}
}
Main.java:4: error: bad operand types for binary operator '+'
        return a + b;
                 ^
  first type:  T
  second type: T
  where T is a type-variable:
    T extends Number declared in method <T>add(T,T)
1 error
using System.Numerics;
Console.WriteLine(Add(2, 3));
Console.WriteLine(Add(1.5, 2.25));
// INumber<T> (C# 11) says "T has + and gives a T back".
static T Add<T>(T a, T b) where T : INumber<T>
{
return a + b;
}
$ dotnet run✓ runs
5
3.75
#include <stdio.h>
// C has no generics. A macro pastes the text, and the compiler checks the result.
#define ADD(a, b) ((a) + (b))
int main(void) {
printf("%d\n", ADD(2, 3));
printf("%g\n", ADD(1.5, 2.25));
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
5
3.75
#include <iostream>
// No constraint: the body is checked when add is used with a type.
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(2, 3) << "\n";
std::cout << add(1.5, 2.25) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
5
3.75
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:4:5
  |
3 | fn add<T: Add>(a: T, b: T) -> T {
  |        -                      - expected `T` because of return type
  |        |
  |        expected this type parameter
4 |     a + b
  |     ^^^^^ expected type parameter `T`, found associated type
  |
  = note: expected type parameter `T`
            found associated type `<T as Add>::Output`
help: consider further restricting this bound
  |
3 | fn add<T: Add<Output = T>>(a: T, b: T) -> T {
  |              ++++++++++++
What happens in Go

Go prints 5 and 3.75. The constraint int | float64 lists the allowed types, and + on a T gives a T back, so nothing more needs to be said.

What happens in Kotlin

Kotlin refuses too. Number has no plus, so a + b on a T : Number finds nothing to call.

What happens in Python

Python prints 5 and 3.75. The interpreter does not check the [T] hint when the program runs, so a + b is tried with whatever arrives. A type checker such as mypy would reject a + b on an unbounded T; this site does not run one.

What happens in TypeScript

TypeScript refuses too, in terms close to Rust's: the sum is a number, and T could be a narrower type, so it cannot be returned as a T.

What happens in Java

Java refuses too (bad operand types for binary operator '+'). Number has no +, and a type variable cannot supply one.

What happens in C#

C# prints 5 and 3.75. The INumber<T> constraint (C# 11) says T has + and that adding two T gives a T, so the promise is in the constraint.

What happens in C

C prints 5 and 3.75. The macro pastes ((2) + (3)) and ((1.5) + (2.25)) before the compiler looks at types, so there is no generic function to check.

What happens in C++

C++ prints 5 and 3.75. With no constraint, the template body is checked when it is used with int and with double. A C++20 concept would check the call, not the body. Rust checks the body once, against the bound.

What Rust sees

T: Add says only that two T values can be added. The type of the sum is a separate type, <T as Add>::Output, and nothing says it is T. So the compiler refuses to return it as a T.

Why Rust says no

In Rust, + calls the add method of the Add trait, and each implementation chooses its own result type. For i32 it is i32, but for &i32 it is also i32, not &i32. Add<Output = T> promises that the sum is a T, and the compiler's help: writes it for you. The toolkit below prints the types.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
use std::ops::Add;
// The sum of two Ts must also be a T: `Output = T` says so.
fn add<T: Add<Output = T>>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("{}", add(2, 3));
println!("{}", add(1.5, 2.25));
}
$ cargo run
5
3.75

The toolkit

Generic functions

A generic function is written once with a placeholder for a type, usually T, and the compiler fills it in at each call. Reading signatures shows where the <T> goes; this lesson is about what it does.

Rustsrc/main.rsRun ↗
// T is a placeholder: the compiler fills it in at each call.
fn first_and_last<T>(items: &[T]) -> Option<(&T, &T)> {
Some((items.first()?, items.last()?))
}
// T appears only in the result, so the caller has to say which one.
fn zero<T: Default>() -> T {
T::default()
}
fn main() {
println!("numbers: {:?}", first_and_last(&[4, 8, 15, 16]));
println!("words: {:?}", first_and_last(&["ant", "bee", "cat"]));
println!("nothing: {:?}", first_and_last::<f64>(&[]));
println!("zero::<i32>(): {}", zero::<i32>());
let name: String = zero();
println!("zero() as a String: {name:?}");
}
$ cargo run
numbers: Some((4, 16))
words: Some(("ant", "cat"))
nothing: None
zero::<i32>(): 0
zero() as a String: ""
  • T is worked out from the arguments. &[4, 8, 15, 16] makes it i32, and the words make it &str.
  • It returns references into the slice, not T itself: a generic T may not be Copy, and moving an item out of a slice is refused (Reading signatures runs E0508).
  • An empty list says nothing about T, so first_and_last::<f64>(&[]) names it with the turbofish (::<>).
  • T: Default is a bound: “T must have a default value”. Bounds get their own section below.

zero mentions T only in its result, so a call with nothing to go on is an error:

Rustsrc/main.rs✗ won't compile
fn zero<T: Default>() -> T {
T::default()
}
fn main() {
let n = zero();
println!("{n}");
}
$ cargo buildWhat is E0283?
error[E0283]: type annotations needed
 --> src/main.rs:6:9
  |
6 |     let n = zero();
  |         ^   ------ type must be known at this point
  |
  = note: the type must implement `Default`
note: required by a bound in `zero`
 --> src/main.rs:1:12
  |
1 | fn zero<T: Default>() -> T {
  |            ^^^^^^^ required by this bound in `zero`
help: consider giving `n` an explicit type
  |
6 |     let n: /* Type */ = zero();
  |          ++++++++++++

help: offers a type on the let; zero::<i32>() does the same job. This is E0283. Vec::new() with no use gave E0282 in Declarations and types; both are the “type annotations needed” family.

Generic structs and enums

Structs and enums take type parameters the same way. Vec<T>, Option<T> and Result<T, E> are ordinary types declared like this, and Maybe<T> below has the shape of Option<T>.

Rustsrc/main.rsRun ↗
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
// `impl<T>` declares T, then `Pair<T>` uses it: every Pair gets these methods.
impl<T> Pair<T> {
fn new(first: T, second: T) -> Pair<T> {
Pair { first, second }
}
fn swapped(self) -> Pair<T> {
Pair::new(self.second, self.first)
}
// A method can add parameters of its own, after its name.
fn map<U>(self, f: impl Fn(T) -> U) -> Pair<U> {
Pair::new(f(self.first), f(self.second))
}
}
// No T here: this method exists only on Pair<f64>.
impl Pair<f64> {
fn sum(&self) -> f64 {
self.first + self.second
}
}
// Two parameters, which may be different types.
struct Entry<K, V> {
key: K,
value: V,
}
#[derive(Debug)]
enum Maybe<T> {
Nothing,
Just(T),
}
fn main() {
println!("ints: {:?}", Pair::new(1, 2).swapped());
println!("mapped: {:?}", Pair::new(1, 2).map(|n| format!("#{n}")));
println!("words: {:?}", Pair::new("a", "b"));
println!("sum: {}", Pair::new(1.5, 2.0).sum());
let entry = Entry {
key: "id",
value: 7,
};
println!("entry: {} -> {}", entry.key, entry.value);
let found: Maybe<char> = Maybe::Just('x');
let missing: Maybe<char> = Maybe::Nothing;
println!("maybe: {found:?} and {missing:?}");
}
$ cargo run
ints: Pair { first: 2, second: 1 }
mapped: Pair { first: "#1", second: "#2" }
words: Pair { first: "a", second: "b" }
sum: 3.5
entry: id -> 7
maybe: Just('x') and Nothing
  • In impl<T> Pair<T>, the first <T> introduces the placeholder and the second says which type the block is for.
  • A method can add parameters of its own after its name: map<U> turns a Pair<T> into a Pair<U>, and impl Fn(T) -> U takes a closure.
  • impl Pair<f64> names one concrete type, so sum exists only for pairs of f64.
  • Entry<K, V> has two parameters, the same type or different ones.

Bounds: what T may do

Inside a generic body, T could be any type, so a bare T has no {}, no == and no .clone(): the body can move it, borrow it and store it. Anything more must be asked for in a bound, the name of a trait (an ability a type can have):

Rustsrc/main.rs✗ won't compile
// Nothing is asked of T, so a body may not print it or compare it.
fn bracket<T>(item: T) -> String {
format!("[{item}]")
}
fn same<T>(a: T, b: T) -> bool {
a == b
}
fn main() {
println!("{} {}", bracket(5), same(1, 2));
}
$ cargo buildWhat is E0277?
error[E0277]: `T` doesn't implement `std::fmt::Display`
 --> src/main.rs:3:15
  |
3 |     format!("[{item}]")
  |               ^^^^^^ `T` cannot be formatted with the default formatter
  |
  = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead
help: consider restricting type parameter `T` with trait `Display`
  |
2 | fn bracket<T: std::fmt::Display>(item: T) -> String {
  |             +++++++++++++++++++

error[E0369]: binary operation `==` cannot be applied to type `T`
 --> src/main.rs:7:7
  |
7 |     a == b
  |     - ^^ - T
  |     |
  |     T
  |
help: consider restricting type parameter `T` with trait `PartialEq`
  |
6 | fn same<T: std::cmp::PartialEq>(a: T, b: T) -> bool {
  |          +++++++++++++++++++++

Some errors have detailed explanations: E0277, E0369.

The help: writes the bound for you. These are the ones you will ask for most; Common traits says what each means.

To do this with a T Ask for
print with {}, with {:?} Display, Debug
compare with ==; with < and > PartialEq; PartialOrd
sort(), max(), use as a BTreeSet item Ord
use as a HashMap key or HashSet item Eq + Hash
call .clone() Clone
use a value twice without moving it Copy
a + b giving a T Add<Output = T>
T::default() Default
turn a u8 or u32 into a u64 Into<u64>

f64 has PartialOrd but not Ord: NaN is neither less than nor greater than anything, so sort() on floats is refused (Common traits). Leave a needed bound out and the error is inside the body; pass a type that lacks it and the error is at the call (Reading signatures shows both).

a + b is the odd row, and the gotcha above is about it. T: Add says two Ts can be added, but the sum has a type of its own, T::Output, and for &i32 it is i32:

Rustsrc/main.rsRun ↗
use std::any::type_name;
use std::ops::Add;
// `T: Add` says two Ts can be added. The result type is T::Output.
fn sum_type<T: Add>() -> String {
format!(
"T = {:<5} T + T = {}",
type_name::<T>(),
type_name::<T::Output>()
)
}
fn main() {
println!("{}", sum_type::<i32>());
println!("{}", sum_type::<f64>());
println!("{}", sum_type::<&i32>());
}
$ cargo run
T = i32   T + T = i32
T = f64   T + T = f64
T = &i32  T + T = i32

Add<Output = T> closes the gap: “adding two Ts gives a T”.

Several bounds, where and impl Trait

Rustsrc/main.rsRun ↗
use std::fmt::{Debug, Display};
// Two bounds joined with `+`.
fn report<T: PartialOrd + Display>(items: &[T]) -> String {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
format!("largest of {}: {best}", items.len())
}
// Two `impl` parameters are two separate hidden type parameters.
fn pair_impl(a: impl Display, b: impl Display) -> String {
format!("{a} and {b}")
}
// Each parameter can have its own bounds; `where` keeps the signature readable.
fn both<A, B>(shown: A, debugged: B) -> String
where
A: Display,
B: Debug,
{
format!("{shown} and {debugged:?}")
}
fn main() {
println!("{}", report(&[3, 9, 2]));
println!("{}", report(&["kiwi", "apple", "pear"]));
println!("pair_impl: {}", pair_impl(1, "one"));
println!("both: {}", both("text", vec![Some(1), None]));
}
$ cargo run
largest of 3: 9
largest of 3: pear
pair_impl: 1 and one
both: text and [Some(1), None]

+ asks for all the bounds, and where moves them after the signature (Traits shows all three forms). Each impl parameter is its own hidden type parameter, so pair_impl(1, "one") works. With one T for both the arguments must match:

Rustsrc/main.rs✗ won't compile
use std::fmt::Display;
// One T for both parameters: the two arguments must have the same type.
fn pair<T: Display>(a: T, b: T) -> String {
format!("{a} and {b}")
}
fn main() {
println!("{}", pair(1, "one"));
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:9:28
  |
9 |     println!("{}", pair(1, "one"));
  |                    ---- -  ^^^^^ expected integer, found `&str`
  |                    |    |
  |                    |    expected all arguments to be this integer type because they need to match the type of this parameter
  |                    arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:4:4
  |
4 | fn pair<T: Display>(a: T, b: T) -> String {
  |    ^^^^ -           ----  ---- this parameter needs to match the integer type of `a`
  |         |           |
  |         |           `b` needs to match the integer type of this parameter
  |         `a` and `b` both reference this parameter `T`

A hidden parameter has no name to write, so the turbofish refuses it:

Rustsrc/main.rs✗ won't compile
use std::fmt::Display;
fn named<T: Display>(x: T) -> String {
format!("{x}")
}
fn hidden(x: impl Display) -> String {
format!("{x}")
}
fn main() {
println!("{}", named::<i32>(1));
println!("{}", hidden::<i32>(1));
}
$ cargo buildWhat is E0107?
error[E0107]: function takes 0 generic arguments but 1 generic argument was supplied
  --> src/main.rs:13:20
   |
13 |     println!("{}", hidden::<i32>(1));
   |                    ^^^^^^------- help: remove the unnecessary generics
   |                    |
   |                    expected 0 generic arguments
   |
note: function defined here, with 0 generic parameters
  --> src/main.rs:7:4
   |
 7 | fn hidden(x: impl Display) -> String {
   |    ^^^^^^
   = note: `impl Trait` cannot be explicitly specified as a generic argument

Use T when parameters must match or callers may need the turbofish, impl when you never name the type.

Methods for some T only

A bound can sit on an impl block, so its methods exist only for types that qualify:

Rustsrc/main.rsRun ↗
use std::fmt::Display;
struct Wrapper<T>(T);
// For every T.
impl<T> Wrapper<T> {
fn into_inner(self) -> T {
self.0
}
}
// Only when T can be displayed.
impl<T: Display> Wrapper<T> {
fn show(&self) -> String {
format!("[{}]", self.0)
}
}
fn main() {
println!("number: {}", Wrapper(42).show());
println!("text: {}", Wrapper("hi").show());
let list = Wrapper(vec![1, 2]);
println!("a list still unwraps: {:?}", list.into_inner());
}
$ cargo run
number: [42]
text: [hi]
a list still unwraps: [1, 2]

A list has no show; the error says a bound is missing and points at the impl that asks for it:

Rustsrc/main.rs✗ won't compile
use std::fmt::Display;
struct Wrapper<T>(T);
impl<T: Display> Wrapper<T> {
fn show(&self) -> String {
format!("[{}]", self.0)
}
}
fn main() {
println!("{}", Wrapper(vec![1, 2]).show());
}
$ cargo buildWhat is E0599?
error[E0599]: the method `show` exists for struct `Wrapper<Vec<{integer}>>`, but its trait bounds were not satisfied
  --> src/main.rs:12:40
   |
 3 | struct Wrapper<T>(T);
   | ----------------- method `show` not found for this struct
...
12 |     println!("{}", Wrapper(vec![1, 2]).show());
   |                                        ^^^^ method cannot be called on `Wrapper<Vec<{integer}>>` due to unsatisfied trait bounds
   |
note: trait bound `Vec<{integer}>: std::fmt::Display` was not satisfied
  --> src/main.rs:5:9
   |
 5 | impl<T: Display> Wrapper<T> {
   |         ^^^^^^^  ----------
   |         |
   |         unsatisfied trait bound introduced here

Put bounds on the impl that needs them, not on the struct. A bound on the struct is demanded of every impl that mentions it, even one whose methods do not use it:

Rustsrc/main.rs✗ won't compile
use std::fmt::Display;
// The bound is on the struct, so every impl has to repeat it.
struct Labeled<T: Display> {
value: T,
}
impl<T> Labeled<T> {
fn value(&self) -> &T {
&self.value
}
}
fn main() {
let label = Labeled { value: 7 };
println!("{}", label.value());
}
$ cargo buildWhat is E0277?
error[E0277]: `T` doesn't implement `std::fmt::Display`
 --> src/main.rs:8:9
  |
8 | impl<T> Labeled<T> {
  |         ^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `T`
  |
note: required by a bound in `Labeled`
 --> src/main.rs:4:19
  |
4 | struct Labeled<T: Display> {
  |                   ^^^^^^^ required by this bound in `Labeled`
help: consider restricting type parameter `T` with trait `Display`
  |
8 | impl<T: std::fmt::Display> Labeled<T> {
  |       +++++++++++++++++++

error[E0277]: `T` doesn't implement `std::fmt::Display`
 --> src/main.rs:9:15
  |
9 |     fn value(&self) -> &T {
  |               ^^^^ the trait `std::fmt::Display` is not implemented for `T`
  |
note: required by a bound in `Labeled`
 --> src/main.rs:4:19
  |
4 | struct Labeled<T: Display> {
  |                   ^^^^^^^ required by this bound in `Labeled`
help: consider restricting type parameter `T` with trait `Display`
  |
8 | impl<T: std::fmt::Display> Labeled<T> {
  |       +++++++++++++++++++

The API Guidelines agree: Data structures do not duplicate derived trait bounds.

The same works for a trait: impl<T: Display> Shout for T gives Shout to every type that meets the bound, types written later too. This blanket impl runs in Traits; ToString is one.

Traits with their own parameters

A trait can take type parameters too, and one type can implement it once for each:

Rustsrc/main.rsRun ↗
// A trait can take a type parameter too.
trait Convert<T> {
fn convert(&self) -> T;
}
struct Celsius(f64);
impl Convert<f64> for Celsius {
fn convert(&self) -> f64 {
self.0 * 9.0 / 5.0 + 32.0
}
}
impl Convert<String> for Celsius {
fn convert(&self) -> String {
format!("{} degrees C", self.0)
}
}
fn main() {
let temp = Celsius(100.0);
// The type you ask for picks the impl.
let fahrenheit: f64 = temp.convert();
let label: String = temp.convert();
println!("as f64: {fahrenheit}");
println!("as String: {label}");
}
$ cargo run
as f64: 212
as String: 100 degrees C

The type you ask for picks the impl.

A parameter can have a default. Add is Add<Rhs = Self>, so impl Add for Meters means Add<Meters>, and a bare T: Add means Add<T>, which is why the gotcha can add two Ts.

Rustsrc/main.rsRun ↗
use std::ops::Add;
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
// `Add` is declared `Add<Rhs = Self>`: a type parameter with a default.
// Writing `Add` alone means `Add<Meters>`.
impl Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
// The same type can implement it again for another right-hand side.
impl Add<f64> for Meters {
type Output = Meters;
fn add(self, other: f64) -> Meters {
Meters(self.0 + other)
}
}
fn main() {
println!("Meters + Meters: {:?}", Meters(1.0) + Meters(2.0));
println!("Meters + f64: {:?}", Meters(1.0) + 0.5);
}
$ cargo run
Meters + Meters: Meters(3.0)
Meters + f64: Meters(1.5)

When a type should have only one answer, a trait uses an associated type instead, as Add does for Output and Iterator does for Item (Traits).

Const generics

A const generic is a number, not a type, known when the program is compiled. It is how a function or type works with arrays of any length:

Rustsrc/main.rsRun ↗
use std::any::type_name;
// N is a number the compiler knows, filled in from the array's type.
fn sum<const N: usize>(values: [i32; N]) -> i32 {
println!(" an array of {N}");
values.iter().sum()
}
struct Buffer<const N: usize> {
data: [u8; N],
}
impl<const N: usize> Buffer<N> {
fn new() -> Buffer<N> {
Buffer { data: [0; N] }
}
fn capacity(&self) -> usize {
self.data.len()
}
}
fn main() {
println!("sum: {}", sum([1, 2, 3]));
println!("sum: {}", sum([10, 20, 30, 40, 50]));
println!("sum of none: {}", sum([]));
println!("capacity: {}", Buffer::<16>::new().capacity());
println!("type of [i32; 3]: {}", type_name::<[i32; 3]>());
println!("type of [i32; 4]: {}", type_name::<[i32; 4]>());
}
$ cargo run
  an array of 3
sum: 6
  an array of 5
sum: 150
  an array of 0
sum of none: 0
capacity: 16
type of [i32; 3]: [i32; 3]
type of [i32; 4]: [i32; 4]

[i32; 3] and [i32; 4] are different types, so sum takes the length as a parameter. N can be used as a value inside, and 0 is allowed. A const parameter may be an integer, char or bool (Reference). For a length known only at run time, use a slice or a Vec (Vectors).

Your language’s generics

Pick your language in “Coming from” at the top. Every row below is compiled in the image that builds this page, in Rust and in the other language.

Going deeper

One copy per type

The compiler writes a separate copy of a generic function for each type it is used with. This is called monomorphization. Each copy knows its T: size_of::<T>() is a plain number in it, and < or + is the operation for that type, chosen when compiling. type_name::<T>() below is for diagnostics only: the standard library says its text is not specified.

Rustsrc/main.rsRun ↗
use std::any::type_name;
use std::mem::size_of;
fn describe<T>() {
println!(
"T = {:<30} size_of::<T>() = {}",
type_name::<T>(),
size_of::<T>()
);
}
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut best = &items[0];
for item in items {
if item > best {
best = item;
}
}
best
}
fn main() {
// The same function body, once per T, and each copy knows its T.
describe::<u8>();
describe::<u64>();
describe::<String>();
describe::<&str>();
describe::<Option<u32>>();
let for_ints = largest::<i32> as *const ();
let for_floats = largest::<f64> as *const ();
println!(
"largest::<i32> and largest::<f64> are different functions: {}",
for_ints != for_floats
);
}
$ cargo run
T = u8                             size_of::<T>() = 1
T = u64                            size_of::<T>() = 8
T = alloc::string::String          size_of::<T>() = 24
T = &str                           size_of::<T>() = 16
T = core::option::Option<u32>      size_of::<T>() = 8
largest::<i32> and largest::<f64> are different functions: true

The last line compares the addresses of two instances of largest: different functions. The Book says the result runs as if you had written each copy by hand (Performance of Code Using Generics). The price is more compiled code, one copy per type. This page measures no speed, binary size or compile time, and the runner builds only debug builds.

Because a copy knows its T, a generic struct stores its fields inline:

Pair<i32> and Pair<String>
STACKHEAPnumbers: Pair<i32>first 1second 28 bytes: two numbers side by sidenames.first: Stringptrlen 3cap 3names is a Pair<String>: 48 bytes, two of these side by sidenames.second: Stringptrlen 2cap 2"ana"ana"bo"bo
The same struct, two layouts. The size of Pair follows from the size of T. The letters of a String live on the heap; the two Strings of a Pair sit next to each other.
Rustsrc/main.rsRun ↗
use std::mem::size_of;
struct Pair<T> {
first: T,
second: T,
}
fn main() {
println!("size_of::<i32>(): {}", size_of::<i32>());
println!("size_of::<Pair<i32>>(): {}", size_of::<Pair<i32>>());
println!("size_of::<String>(): {}", size_of::<String>());
println!("size_of::<Pair<String>>(): {}", size_of::<Pair<String>>());
let numbers = Pair {
first: 1,
second: 2,
};
let names = Pair {
first: String::from("ana"),
second: String::from("bo"),
};
println!("numbers: {} and {}", numbers.first, numbers.second);
println!("names: {} and {}", names.first, names.second);
println!(
"first name: len {}, capacity {}",
names.first.len(),
names.first.capacity()
);
println!(
"second name: len {}, capacity {}",
names.second.len(),
names.second.capacity()
);
}
$ cargo run
size_of::<i32>(): 4
size_of::<Pair<i32>>(): 8
size_of::<String>(): 24
size_of::<Pair<String>>(): 48
numbers: 1 and 2
names: ana and bo
first name: len 3, capacity 3
second name: len 2, capacity 2

T has a known size, unless you say otherwise

Each copy must know how big its T is, so every type parameter carries a hidden bound, Sized. The text behind a &str is a str, whose length is known only when the program runs, so it cannot be a T:

Rustsrc/main.rs✗ won't compile
use std::fmt::Display;
fn show<T: Display>(item: &T) -> String {
format!("<{item}>")
}
fn main() {
println!("{}", show("hello"));
}
$ cargo buildWhat is E0277?
error[E0277]: the size for values of type `str` cannot be known at compilation time
 --> src/main.rs:8:25
  |
8 |     println!("{}", show("hello"));
  |                    ---- ^^^^^^^ doesn't have a size known at compile-time
  |                    |
  |                    required by a bound introduced by this call
  |
  = help: the trait `Sized` is not implemented for `str`
note: required by an implicit `Sized` bound in `show`
 --> src/main.rs:3:9
  |
3 | fn show<T: Display>(item: &T) -> String {
  |         ^ required by the implicit `Sized` requirement on this type parameter in `show`
help: consider relaxing the implicit `Sized` restriction
  |
3 | fn show<T: Display + ?Sized>(item: &T) -> String {
  |                    ++++++++

?Sized (“may or may not have a known size”) lifts the rule. A & has a known size, so a reference to the value is fine:

Rustsrc/main.rsRun ↗
use std::fmt::Display;
use std::mem::size_of_val;
// `?Sized` lifts the hidden "T has a known size" rule.
fn show<T: Display + ?Sized>(item: &T) -> String {
format!("<{item}> is {} bytes", size_of_val(item))
}
fn main() {
println!("{}", show("hello"));
println!("{}", show(&5_i64));
}
$ cargo run
<hello> is 5 bytes
<5> is 8 bytes

You will meet T: ?Sized in standard library signatures: ToString is implemented for every T: Display + ?Sized.

What T is at run time, in your language

In Rust each copy is for one concrete type, so no generic code is left at run time and there is no reflection on T: you cannot list its members or rely on its name. You can ask whether two types are the same, for 'static types (types that hold no borrowed data), and you call T’s behaviour through trait functions such as T::default():

Rustsrc/main.rsRun ↗
use std::any::{Any, TypeId};
// `T: 'static` means T holds no borrowed data. TypeId works for such types.
fn same_type<A: 'static, B: 'static>() -> bool {
TypeId::of::<A>() == TypeId::of::<B>()
}
fn is_a<T: 'static>(value: &dyn Any) -> bool {
value.is::<T>()
}
fn main() {
println!("same_type::<i32, i32>(): {}", same_type::<i32, i32>());
println!("same_type::<i32, u8>(): {}", same_type::<i32, u8>());
println!("is_a::<i32>(&3i32): {}", is_a::<i32>(&3i32));
println!("is_a::<i32>(&\"x\"): {}", is_a::<i32>(&"x"));
}
$ cargo run
same_type::<i32, i32>(): true
same_type::<i32, u8>(): false
is_a::<i32>(&3i32): true
is_a::<i32>(&"x"): false

Other languages keep more or less of T:

When to make something generic

Write the concrete version first. Make it generic when a second type needs the same code, and ask for only the bounds the body uses: every extra bound narrows who can call it, as the exercises check.

Generics or trait objects

A generic parameter is one type per use. Vec<T> holds only Ts, and a list of mixed types is refused:

Rustsrc/main.rs✗ won't compile
fn main() {
let items = vec![1, "two", 3.0];
println!("{}", items.len());
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:2:25
  |
2 |     let items = vec![1, "two", 3.0];
  |                         ^^^^^ expected integer, found `&str`

When one list must hold different types that share an ability, you use a trait object (dyn Trait): the compiler no longer picks a copy, and the method is looked up while the program runs. Trait objects covers it, and what it costs compared with generics.

Practice

Try it

Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run until it prints all checks passed.

Smallest and largest

Return references to the smallest and the largest item of a slice, or None if it is empty. The checks use whole numbers, decimals, &str, char and a Version type that supports < and > and nothing else. Decimals are not Ord, and all of them are negative, so starting from a zero does not work either.

Starterexercises/min-max.rs
// The smallest and the largest item of a slice, or None if it is empty.
// The checks use whole numbers, decimals, &str, char and a type with almost no traits.
fn min_max<T>(items: &[T]) -> Option<(&T, &T)> {
todo!("the compiler will name the bound you need; start from the first item")
}
// Comparable with < and >, and nothing else: no Copy, Clone, Debug or Ord.
#[derive(PartialEq, PartialOrd)]
struct Version(u32, u32);
fn main() {
assert_eq!(min_max(&[3, 9, 2, 7]), Some((&2, &9)));
assert_eq!(min_max(&[1, 5, 9]), Some((&1, &9)));
assert_eq!(min_max(&[9, 5, 1]), Some((&1, &9)));
assert_eq!(min_max(&[4]), Some((&4, &4)));
// Decimals can be compared with < but are not Ord; all negative on purpose.
assert_eq!(min_max(&[-4.5, -0.5, -9.25]), Some((&-9.25, &-0.5)));
assert_eq!(
min_max(&["kiwi", "apple", "pear"]),
Some((&"apple", &"pear"))
);
assert_eq!(min_max(&['z', 'a']), Some((&'a', &'z')));
assert_eq!(min_max::<i32>(&[]), None);
let versions = [Version(1, 4), Version(0, 9), Version(2, 0), Version(1, 10)];
let (lowest, highest) = min_max(&versions).unwrap();
assert_eq!((lowest.0, lowest.1), (0, 9));
assert_eq!((highest.0, highest.1), (2, 0));
// The answer is an item of the slice itself, not a copy.
assert!(std::ptr::eq(lowest, &versions[1]));
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/min-max.solution.rsRun ↗
// The smallest and the largest item of a slice, or None if it is empty.
// The checks use whole numbers, decimals, &str, char and a type with almost no traits.
fn min_max<T: PartialOrd>(items: &[T]) -> Option<(&T, &T)> {
let mut smallest = items.first()?;
let mut largest = smallest;
for item in items {
if item < smallest {
smallest = item;
}
if item > largest {
largest = item;
}
}
Some((smallest, largest))
}
// Comparable with < and >, and nothing else: no Copy, Clone, Debug or Ord.
#[derive(PartialEq, PartialOrd)]
struct Version(u32, u32);
fn main() {
assert_eq!(min_max(&[3, 9, 2, 7]), Some((&2, &9)));
assert_eq!(min_max(&[1, 5, 9]), Some((&1, &9)));
assert_eq!(min_max(&[9, 5, 1]), Some((&1, &9)));
assert_eq!(min_max(&[4]), Some((&4, &4)));
// Decimals can be compared with < but are not Ord; all negative on purpose.
assert_eq!(min_max(&[-4.5, -0.5, -9.25]), Some((&-9.25, &-0.5)));
assert_eq!(
min_max(&["kiwi", "apple", "pear"]),
Some((&"apple", &"pear"))
);
assert_eq!(min_max(&['z', 'a']), Some((&'a', &'z')));
assert_eq!(min_max::<i32>(&[]), None);
let versions = [Version(1, 4), Version(0, 9), Version(2, 0), Version(1, 10)];
let (lowest, highest) = min_max(&versions).unwrap();
assert_eq!((lowest.0, lowest.1), (0, 9));
assert_eq!((highest.0, highest.1), (2, 0));
// The answer is an item of the slice itself, not a copy.
assert!(std::ptr::eq(lowest, &versions[1]));
println!("all checks passed");
}
$ cargo run
all checks passed

A stack of anything

Fill in Stack<T>: push, pop, peek and len, plus show, which prints the items as [1, 2, 3]. The checks push more than three items and include a type that cannot be printed.

Starterexercises/stack.rs
// A stack of any type: the last item pushed is the first one popped.
use std::fmt::Display;
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Stack<T> {
todo!("an empty stack")
}
fn push(&mut self, item: T) {
todo!("add to the end of items")
}
fn pop(&mut self) -> Option<T> {
todo!("take from the end of items")
}
fn peek(&self) -> Option<&T> {
todo!("look at the end of items without taking it")
}
fn len(&self) -> usize {
todo!("how many items")
}
// Looks like "[1, 2, 3]", bottom of the stack first.
fn show(&self) -> String {
todo!("join the items with \", \" inside brackets")
}
}
// No Display, Clone or Debug: a Stack must still hold it.
struct Token {
id: u32,
}
fn main() {
let mut numbers = Stack::new();
assert_eq!(numbers.pop(), None);
numbers.push(1);
numbers.push(2);
numbers.push(3);
assert_eq!(numbers.len(), 3);
assert_eq!(numbers.peek(), Some(&3));
assert_eq!(numbers.len(), 3);
assert_eq!(numbers.show(), "[1, 2, 3]");
assert_eq!(numbers.pop(), Some(3));
assert_eq!(numbers.show(), "[1, 2]");
for n in 3..=6 {
numbers.push(n);
}
assert_eq!(numbers.len(), 6);
assert_eq!(numbers.show(), "[1, 2, 3, 4, 5, 6]");
let mut words = Stack::new();
words.push(String::from("ana"));
words.push(String::from("bo"));
assert_eq!(words.show(), "[ana, bo]");
assert_eq!(words.pop(), Some(String::from("bo")));
let empty: Stack<char> = Stack::new();
assert_eq!(empty.show(), "[]");
let mut tokens = Stack::new();
tokens.push(Token { id: 7 });
assert_eq!(tokens.peek().map(|t| t.id), Some(7));
let token = tokens.pop().unwrap();
assert_eq!(token.id, 7);
assert_eq!(tokens.len(), 0);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/stack.solution.rsRun ↗
// A stack of any type: the last item pushed is the first one popped.
use std::fmt::Display;
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Stack<T> {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn len(&self) -> usize {
self.items.len()
}
}
// Only stacks of displayable items get `show`.
impl<T: Display> Stack<T> {
fn show(&self) -> String {
let mut text = String::from("[");
for (i, item) in self.items.iter().enumerate() {
if i > 0 {
text.push_str(", ");
}
text.push_str(&item.to_string());
}
text.push(']');
text
}
}
// No Display, Clone or Debug: a Stack must still hold it.
struct Token {
id: u32,
}
fn main() {
let mut numbers = Stack::new();
assert_eq!(numbers.pop(), None);
numbers.push(1);
numbers.push(2);
numbers.push(3);
assert_eq!(numbers.len(), 3);
assert_eq!(numbers.peek(), Some(&3));
assert_eq!(numbers.len(), 3);
assert_eq!(numbers.show(), "[1, 2, 3]");
assert_eq!(numbers.pop(), Some(3));
assert_eq!(numbers.show(), "[1, 2]");
for n in 3..=6 {
numbers.push(n);
}
assert_eq!(numbers.len(), 6);
assert_eq!(numbers.show(), "[1, 2, 3, 4, 5, 6]");
let mut words = Stack::new();
words.push(String::from("ana"));
words.push(String::from("bo"));
assert_eq!(words.show(), "[ana, bo]");
assert_eq!(words.pop(), Some(String::from("bo")));
let empty: Stack<char> = Stack::new();
assert_eq!(empty.show(), "[]");
let mut tokens = Stack::new();
tokens.push(Token { id: 7 });
assert_eq!(tokens.peek().map(|t| t.id), Some(7));
let token = tokens.pop().unwrap();
assert_eq!(token.id, 7);
assert_eq!(tokens.len(), 0);
println!("all checks passed");
}
$ cargo run
all checks passed

The first N items

Return the first N items of a slice as an array, filled up with the default value when the slice is shorter. The caller picks N with a type: let a: [i32; 3] = first_n(&items). The checks use numbers, String and a type with very few traits: choose the bounds the body needs and no more.

Starterexercises/first-n.rs
// The first N items of a slice as an array; if the slice is shorter, the rest is
// the default value. The caller picks N, usually with a type: `let a: [i32; 3] = ...`.
fn first_n<T, const N: usize>(items: &[T]) -> [T; N] {
todo!("`std::array::from_fn(|i| ...)` builds an array of length N")
}
// A type with very few traits: no Copy, no Debug, no PartialEq.
#[derive(Clone, Default)]
struct Tally(u32);
fn main() {
let a: [i32; 3] = first_n(&[1, 2, 3, 4, 5]);
assert_eq!(a, [1, 2, 3]);
let b: [i32; 4] = first_n(&[7]);
assert_eq!(b, [7, 0, 0, 0]);
let c: [char; 3] = first_n(&['x', 'y', 'z']);
assert_eq!(c, ['x', 'y', 'z']);
let none: [char; 0] = first_n(&['x']);
assert_eq!(none, []);
// Strings are Clone but not Copy; the default is the empty string.
let d: [String; 2] = first_n(&[String::from("ana")]);
assert_eq!(d, ["ana", ""]);
let e: [Tally; 2] = first_n(&[Tally(5)]);
assert_eq!((e[0].0, e[1].0), (5, 0));
// N can also be named with the turbofish.
assert_eq!(first_n::<f64, 2>(&[1.5, 2.5, 3.5]), [1.5, 2.5]);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/first-n.solution.rsRun ↗
// The first N items of a slice as an array; if the slice is shorter, the rest is
// the default value. The caller picks N, usually with a type: `let a: [i32; 3] = ...`.
fn first_n<T: Clone + Default, const N: usize>(items: &[T]) -> [T; N] {
std::array::from_fn(|i| items.get(i).cloned().unwrap_or_default())
}
// A type with very few traits: no Copy, no Debug, no PartialEq.
#[derive(Clone, Default)]
struct Tally(u32);
fn main() {
let a: [i32; 3] = first_n(&[1, 2, 3, 4, 5]);
assert_eq!(a, [1, 2, 3]);
let b: [i32; 4] = first_n(&[7]);
assert_eq!(b, [7, 0, 0, 0]);
let c: [char; 3] = first_n(&['x', 'y', 'z']);
assert_eq!(c, ['x', 'y', 'z']);
let none: [char; 0] = first_n(&['x']);
assert_eq!(none, []);
// Strings are Clone but not Copy; the default is the empty string.
let d: [String; 2] = first_n(&[String::from("ana")]);
assert_eq!(d, ["ana", ""]);
let e: [Tally; 2] = first_n(&[Tally(5)]);
assert_eq!((e[0].0, e[1].0), (5, 0));
// N can also be named with the turbofish.
assert_eq!(first_n::<f64, 2>(&[1.5, 2.5, 3.5]), [1.5, 2.5]);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more