When a trait-specified method is called, the compiler will generate code to dispatch the call to the concrete method implementing the trait specification. Rust supports two types of dispatch:

  • Static dispatch is the default dispatch mode when the concrete type can be determined at compile time.
  • Dynamic dispatch is used when the concrete type implementing the trait is not known at compile time.
pub trait Shape {
    type T;
    fn area(&self) -> Self::T;
}
 
pub struct Point<T> {
    x: T,
    y: T,
}
 
pub struct Rectangle<T> {
    top_left: Point<T>,
    bottom_right: Point<T>,
}
 
impl<T> Shape for Rectangle<T>
where
    T: std::ops::Sub<Output = T> + std::ops::Mul<Output = T> + Copy,
{
    type T = T;
    fn area(&self) -> T {
        let width = self.bottom_right.x - self.top_left.x;
        let height = self.top_left.y - self.bottom_right.y;
 
        width * height
    }
}

Static dispatch

pub fn area_pair_static<S, T> (a: S, b: T) -> (f64, f64) 
where
    S: Shape<T = f64>,
    T: Shape<T = f64>,
{
    (a.area(), b.area())
}
 
pub fn static_dispatch_pair(a: Rectangle<f64>, b: Rectangle<f64>) -> (f64, f64) {
    area_pair_static(a, b)
}

Important

We can use impl Shape<T=f64> instead of two separate parameters S and T for the area_pair_static.

The static dispatch code executed by static_dispatch_pair will lead the compiler to only generate the code for area_pair_static for Rectangle. The generate assembly code can also inline the code for Rectangle::area as shown here

Dynamic Dispatch

pub fn area_pair_dynamic(a: &dyn Shape<T = f64>, b: &dyn Shape<T = f64>) -> (f64, f64) {
    (a.area(), b.area())
}

In the dynamic dispatch case, the function takes two Trait Objects aas parameter, therefore the compiler generate code for the function referencing the trait objects, without any reference to the concrete type.

The layout of the trait object is a tuple of two pointers: the data and vtable pointers like so: