What Happens When Rust Compiles a Generic Function?
Like other programming languages, Rust allows us to work with generics and abstractions.
In Rust, there are two ways we can handle types that implement a particular trait.
- The first is when we use a generic type as a function parameter or return value.
- The second is when we need to work with a concrete type via a single interface, such as a
Vec.
Although both serve the same purpose, Rust handles them differently.
The first approach uses static dispatch, which requires the compiler to know the concrete type at compile time. Meanwhile, the second approach uses dynamic dispatch. We use trait objects to handle this one since the compiler doesn't know the concrete type and size at compile time. However, we won't go deep on this one.
Let's get back to the first approach. So, what does it mean for the compiler to know the concrete type at compile time, and what does the compiler actually do?
Let's take a look at this example:
fn addition(a: T, b: T) -> T
where
T: Add,
{
a + b
}
fn main() {
println!("{}", addition(10, 20));
println!("{}", addition(10.5, 20.5));
}
When the compiler sees addition(10, 20), it can determine that both arguments are i32. Therefore, for this call, the compiler knows that T = i32. At this point, the compiler knows the exact types used for each call.
After the compiler gathers this information, it will continue with a process called monomorphization.
The idea is simple. For each function that uses generic types, Rust will generate a specialized version for the concrete types that are actually used.
Therefore, for the previous example, conceptually, the compiler will produce something similar to:
fn addition_i32(a: i32, b: i32) -> i32 {
a + b
}
fn addition_f64(a: f64, b: f64) -> f64 {
a + b
}
fn main() {
println!("{}", addition_i32(10, 20));
println!("{}", addition_f64(10.5, 20.5));
}
The advantage of using this is that there is no runtime lookup cost, but it will make the compiled code bigger.