Rust Generics, Bounds and Const Generics¶
Rust generics let one definition work with several concrete types, but they do not permit arbitrary operations on an unknown type. This lesson builds a borrowed maximum function, a typed reading and a fixed-length window, using Raspberry Pi fixtures without sensor access.
Prerequisites and outcome¶
Complete crates, modules and visibility, and be comfortable with borrowing and Option. You will declare type and constant parameters, state the operations a function requires, and distinguish a generic implementation from methods for one concrete type. We use existing standard-library traits here; defining your own traits comes next.
Start with the operation, then choose a bound¶
A function that finds the largest i32 can compare values because the compiler knows what i32 supports. Replacing i32 with an unconstrained T removes that knowledge. The signature must state the required comparison behaviour.
Here, T: Ord requires a total ordering. We return a reference to an existing element, so the function needs neither Copy nor Clone. An empty slice produces None instead of panicking. The algorithm retains the first maximum when values compare equal; that is our chosen policy, not a requirement of generics.
The Ord documentation defines the ordering contract. Ordinary f32/f64 values do not implement Ord because of cases such as NaN. Relaxing the bound to PartialOrd would also require deciding what an incomparable value means; changing the bound alone does not design that policy.
Build the complete Rust generic program¶
Keep edition = "2024" in Cargo.toml and replace src/main.rs with the complete program below. No external dependencies are needed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
Expected output:
There are seven tests. The pointer-equality check observes which existing element is borrowed; it neither dereferences a raw pointer nor requires unsafe code.
Type parameters, inference and concrete methods¶
Reading<T> has one parameter. Each use substitutes a concrete type: Reading
The declaration impl<T> introduces the parameter used by Reading<T>. It does not mean that a new generic parameter is chosen every time value() is called. In contrast, impl Reading<i32> adds a method only to that concrete instantiation. It is an inherent implementation, not unstable trait specialization. See the Book's generic data types and methods.
Rust infers T from Reading::new's argument and infers the one-element window's type and length. Window::<i32, 3> and largest::<i32> use explicit generic arguments, sometimes called turbofish syntax. Empty data may provide no element-type information, so add an annotation instead of guessing what the compiler should infer.
Option
Bounds describe requirements, not runtime tests¶
The report function needs both ordering and formatting, expressed with T: Ord + Display. Its where clause is another location for the same bound syntax, useful when signatures become crowded. Bounds give the generic body access to those operations and constrain callers; they are not runtime checks that a particular value is “large enough”. See trait bounds and where clauses.
Ord and Display are standard-library traits; the generic parameters and bounds are language syntax. Bounds do not automatically make an enclosing type implement those traits. Reading
Generic type parameters normally have an implicit Sized bound. A borrowed generic API can sometimes relax it with T: ?Sized; that means Sized is not required, not that the type must be unsized. Our slice elements must have a known size, so this function is not a candidate for that change. Sized describes this distinction; later lessons revisit dynamically sized types.
Returning Option<&T> preserves ownership of the input. The reference cannot outlive its backing storage. This signature uses lifetime elision; explicit lifetime relationships are taught after traits rather than hidden behind claims that generics extend storage.
Const generics encode an array length¶
The parameter const N: usize supplies a compile-time value. Window
Calling as_slice() deliberately erases the fixed length from the borrowed slice type, allowing one largest function to accept different lengths. Choosing Vec
Const generics are not the same as declaring a const item or const fn. On stable Rust, using N directly as an array length works, but an arbitrary generic expression such as [T; N + 1] is not supported by this form. Arithmetic with N in an ordinary function body is a different case. We use no nightly features. For restrictions and parameter kinds, consult the generic-parameter reference.
Generic functions are ordinarily monomorphized for concrete uses by the compiler. This is not an interpreter checking type names for each comparison. It also does not establish a speedup: compilation cost, generated code size and runtime behaviour depend on the actual program. This lesson makes no benchmark claim.
Deliberately failing: an unknown type cannot be compared¶
In a separate scratch project, replace src/main.rs with this complete program:
cargo check reports E0369. Even though main supplies integers, the generic function's body must type-check using its declared requirements. Adding T: PartialOrd makes these particular integer calls compile; choose a total-order bound and an explicit tie policy when that is your actual contract.
Exercises and troubleshooting¶
- Rewrite largest's bound using a where clause: all tests and output should remain unchanged.
- Change the main window to four elements, adding 0, and set N to 4: the maximum stays 60000 and the printed length becomes 4. Change only N to 4 without adding an element: expect E0308.
- Call largest on
[46.7_f64, 60.0]: expect E0277 because f64 does not implement Ord. Do not replace this with an unexamined NaN policy. - Construct Reading
and call celsius(): expect E0599, because that inherent method exists only for Reading . - Remove the type argument in
largest::<i32>(&[]): expect E0283 because neither the empty input nor the comparison gives a unique element type. - Repair the failing larger function with a PartialOrd bound. Verify its integer output is 60000, and explain why a float/NaN call needs a separate semantic decision.
If the error mentions a missing trait bound, identify the operation requiring it. Do not add Copy merely to fix a comparison error: returning a borrow already works with String. If a type parameter is unknown inside an impl, check that it was declared after impl. If an array length mismatches, compare the const argument with the actual element count.
Verification and next step¶
On October 10, 2026, the lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, all seven tests, formatting and debug/release output comparisons passed. The where-clause variant, four-element window and repaired integer comparison also passed. Missing comparison bounds, unavailable concrete methods, floating-point Ord, ambiguous empty input and mismatched array lengths produced the expected E0369, E0599, E0277, E0283 and E0308 diagnostics. These fixtures do not read temperature sensors or measure performance.
Next: trait definitions, implementations and associated types, defining and composing behaviour contracts.