RustArena

Step 5 of 6

Monomorphization: generics cost nothing at run time

How can one largest function work on i32s, f64s and chars, which have different sizes and are compared by different machine instructions? The compiler doesn't keep one flexible version. For every concrete type you actually use, it generates a separate copy of the generic code with T filled in, as if you had written largest_i32, largest_f64 and largest_char yourself. This is called monomorphization ("making into a single form").

You can watch it happen. std::any::type_name::<T>() returns the name of whatever T is in the copy that's running:

fn describe<T: std::fmt::Debug>(value: T) -> String {
    format!("{} = {:?}", std::any::type_name::<T>(), value)
}

fn main() {
    println!("{}", describe(42));
    println!("{}", describe(2.5));
    println!("{}", describe('z'));
    println!("{}", describe("crab"));
    println!("{}", describe(7u8));
    println!("{}", describe::<i64>(7));
}
i32 = 42
f64 = 2.5
char = 'z'
&str = "crab"
u8 = 7
i64 = 7

This program contains six versions of describe: for i32, f64, char, &str, u8 and i64. (std::fmt::Debug is written out in full here instead of with a use line; both work.)

Zero-cost abstraction

Because each copy is specialized, generic code runs exactly as fast as the hand-written versions would: no checking types at run time, no extra indirection. Rust people call this a zero-cost abstraction: you pay nothing at run time for writing the code once.

The cost is paid at compile time instead: every copy has to be compiled, and the program file gets bigger. In large projects heavy generic code can noticeably slow down builds. The alternative is dynamic dispatch, which you've already met in Box<dyn Error>: one copy of the code, and a small run-time lookup to find the right method. Module 25 compares the two.

The turbofish: ::<>

Usually the compiler infers T from the arguments. When it can't, or when you want a different type, write it explicitly with ::<Type>, nicknamed the turbofish:

Common mistakes

fn main() {
    let n = "42".parse().unwrap();
    println!("{n}");
}
error[E0284]: type annotations needed

Fix it with "42".parse::<u32>() or let n: u32 = ....

Your task

Write fn size_info<T>(_value: &T) -> String that returns size of <type name>: <size in bytes>, using std::any::type_name::<T>() and std::mem::size_of::<T>(). The starter's main calls it seven times (seven copies!), the last one with a turbofish. The program prints:

size of u8: 1
size of i32: 4
size of f64: 8
size of char: 4
size of bool: 1
size of (u8, u32): 8
size of u64: 8

(The (u8, u32) tuple takes 8 bytes, not 5: the compiler adds padding so the u32 sits at a position divisible by 4, where the processor reads it fastest.)

Challenge 2

Fill in the turbofish types: parse "42" as a u8, parse "5000000000" as a u64 (too big for a u32), and create an empty Vec of Strings. The program prints:

42 5000000000 ["ferris"]

Challenge 3

Write fn count_matching<T: PartialEq>(items: &[T], target: &T) -> usize that counts how many items are equal to target. The tests call it with four different types, so the compiler builds four copies.

Previous: Methods on generic types: impl<T> Next: Const generics: arrays of any length