Generic functions and structs

In Rust a generic item takes a type as a parameter. The function or struct is written once, and the compiler builds a version for every type it is used with. Rust has no separate template syntax: the angle brackets after the name declare the parameter, and the same parameter appears in the types of the parameters and the fields.

Coming from C++? Templates and generics writes one function for two types in both languages, and then calls a template with a type that lacks the operation: C++ fails only at the instantiation, Rust at the definition.

This lesson stays with unconstrained parameters, which can be moved and stored but not operated on. The next lesson adds the bounds that allow an operation, and the difference between the two is the whole design of generic code in Rust.

A generic function

fn identity<T>(value: T) -> T returns whatever it was given, with the same type. The parameter is declared in angle brackets and used three times: in the argument, in the return type and in the body.

The program calls it with an integer and with a string slice and prints both results. The compiler builds two versions of the function, one per type.

fn identity<T>(value: T) -> T { value } fn main() { println!("{}", identity(42)); println!("{}", identity("rust")); }

rustc --edition 2024 identity.rs
./identity

42
rust

The call site writes no type argument: the compiler infers it from the value. A turbofish, identity::<i32>(42), forces it when inference is ambiguous, which the lesson on associated types uses.

A generic struct

A struct declares the parameter after its name and uses it in the field types. Pair<T> has two fields of the same type, so a pair of integers and a pair of strings are two instantiations of one declaration.

The program builds one of each and prints all four values. The two values are separate types, and nothing is shared between them at run time.

struct Pair<T> { first: T, second: T, } fn main() { let numbers = Pair { first: 1, second: 2 }; let words = Pair { first: "one", second: "two" }; println!("{} {} {} {}", numbers.first, numbers.second, words.first, words.second); }

rustc --edition 2024 pair.rs
./pair

1 2 one two

Two parameters would allow a pair of different types, which is the usual next step: Pair<T, U>. The standard library uses both shapes, for example in HashMap<K, V>.

An impl block with a parameter

An impl block repeats the parameter: impl<T> Pair<T> adds methods that work for every instantiation. The associated function new returns Self, which is the pair of whatever type the caller chooses.

The program builds a pair of two integers through the function and prints both fields. Nothing about the method depends on the type, which is why the same code serves every T.

struct Pair<T> { first: T, second: T, } impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Self { first, second } } } fn main() { let pair = Pair::new(3, 4); println!("{} {}", pair.first, pair.second); }

rustc --edition 2024 impl_pair.rs
./impl_pair

3 4

A method that needs an operation on T adds a bound to the impl block, as the next lesson shows. Without a bound, the body may only move, borrow or store the value.

One function, many types

The compiler writes a separate function for every type a generic is used with; the process is called monomorphisation. The result is the same machine code a hand-written function would have, and the cost is code size rather than run time.

The program builds a tuple of two integers and a tuple of two strings with the same function. The two outputs show that the function did not care about the type.

fn pair_up<T>(first: T, second: T) -> (T, T) { (first, second) } fn main() { let numbers = pair_up(1, 2); let words = pair_up("one", "two"); println!("{} {} {} {}", numbers.0, numbers.1, words.0, words.1); }

rustc --edition 2024 monomorph.rs
./monomorph

1 2 one two

The generated code is not shared between the two calls: each version has its own instructions, and a large generic used with many types makes a larger binary. That is the trade against a dyn object, which keeps one version and pays an indirection per call. A dyn trait object is the alternative when the set of types is not known at compile time; the lesson on trait objects compares the two costs.

A pair of one type

The two fields of Pair<T> have the same type, so a literal with an integer and a string is E0308. The message names the first field as the one that fixed the type and the second as the mismatch.

The program below mixes the two. The fix is either two values of one type or a struct with two parameters, which the exercise asks for.

struct Pair<T> { first: T, second: T, } fn main() { let pair = Pair { first: 1, second: "two" }; println!("{}", pair.first); }

rustc --edition 2024 wrong_type_err.rs

error[E0308]: mismatched types
--> wrong_type_err.rs:7:41
|
7 | let pair = Pair { first: 1, second: "two" };
| ^^^^^ expected integer, found `&str`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

The error is the generic parameter doing its job: one T means one type, and a struct that needs two says so in its declaration.

Exercises

  1. Change Pair<T> to Pair<T, U> and build a pair of an integer and a string.
  2. Write a generic function that returns the first of two values. Which bound does it need, if any?
  3. Call identity with a turbofish and with inference, and compare the two lines.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Where is a type parameter declared in a generic function?

    Answer

    In angle brackets after the function name

    The parameter list comes after the name and is used in the signature and body.

  2. What does monomorphisation produce?

    Answer

    One function per type the generic is used with

    The compiler writes a specialised version for each instantiation, so calls are direct.

  3. What may a generic function do with a T that has no bounds?

    Answer

    Move it, borrow it and store it

    Without a bound the compiler knows nothing about T except that it can be moved.

  4. What does Pair<T> mean for its two fields?

    Answer

    Both fields have the same type

    One parameter means one type for every field that uses it.

  5. What does this program print? fn identity<T>(value: T) -> T { value } fn main() { println!("{}", identity(42)); println!("{}", identity("rust")); }

    Answer

    42 / rust

    The compiler infers T for each call, and the two outputs keep their order.

  6. What does this program print? struct Pair<T> { first: T, second: T, } fn main() { let numbers = Pair { first: 1, second: 2 }; let words = Pair { first: "one", second: "two" }; println!("{} {} {} {}", numbers.first, numbers.second, words.first, words.second); }

    Answer

    1 2 one two

    Each literal creates its own instantiation, and the four fields print in order.

  7. What does this program print? struct Pair<T> { first: T, second: T, } impl<T> Pair<T> { fn new(first: T, second: T) -> Self { Self { first, second } } } fn main() { let pair = Pair::new(3, 4); println!("{} {}", pair.first, pair.second); }

    Answer

    3 4

    The associated function infers T from the two arguments and returns the pair.

  8. What does this program print? fn pair_up<T>(first: T, second: T) -> (T, T) { (first, second) } fn main() { let numbers = pair_up(1, 2); let words = pair_up("one", "two"); println!("{} {} {} {}", numbers.0, numbers.1, words.0, words.1); }

    Answer

    1 2 one two

    The two calls instantiate T as i32 and as &str, and the tuple keeps the values.

  9. What is the first line of the compiler output for this program? struct Pair<T> { first: T, second: T, } fn main() { let pair = Pair { first: 1, second: "two" }; println!("{}", pair.first); }

    Answer

    error[E0308]: mismatched types

    The first field fixes T as an integer, and the second value is a string.

  10. What does this program print? fn swap<T>(a: T, b: T) -> (T, T) { (b, a) } fn main() { let (first, second) = swap(7, 9); println!("{first} {second}"); let (left, right) = swap("a", "b"); println!("{left} {right}"); }

    Answer

    9 7 / b a

    The function returns the two values in the other order, for integers and for strings.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image