The difference between dynamic dispatch and static dispatch in rust

The two traits Base and Derived each define a trait method called method. These methods happen to have the same name but are otherwise unrelated methods as explained below.

Both traits provide a default implementation of their trait method. Default implementations are conceptually copied into each trait impl that does not explicitly define the same method. In this case for example impl Base for BothTraits does not provide its own implementation of Base::method, which means the implementation of Base for BothTraits will use the default behavior defined by the trait i.e. print!("1").

demo code

trait Base {
    fn method(&self) {
        print!("1");
    }
}

trait Derived: Base {
    fn method(&self) {
        print!("2");
    }
}

struct BothTraits;
impl Base for BothTraits {}
impl Derived for BothTraits {}

fn dynamic_dispatch(x: &dyn Base) {
    x.method();
}

fn static_dispatch<T: Base>(x: T) {
    x.method();
}

fn main() {
    dynamic_dispatch(&BothTraits);
    static_dispatch(BothTraits);
}

Additionally, Derived has Base as a supertrait which means that every type that implements Derived is also required to implement Base. The two trait methods are unrelated despite having the same name -- thus any type that implements Derived will have an implementation of Derived::method as well as an implementation of Base::method and the two are free to have different behavior. Supertraits are not inheritance! Supertraits are a constraint that if some trait is implemented, some other trait must also be implemented. Let's consider what happens in each of the two methods called from main.

dynamic_dispatch(&BothTraits)

The argument x is a reference to the trait object type dyn Base. A trait object is a little shim generated by the compiler that implements the trait with the same name by forwarding all trait method calls to trait methods of whatever type the trait object was created from. The forwarding is done by reading from a table of function pointers contained within the trait object.

// Generated by the compiler.
//
// This is an implementation of the trait `Base` for the
// trait object type `dyn Base`, which you can think of as
// a struct containing function pointers.
impl Base for (dyn Base) {
    fn method(&self) {
        /*
        Some automatically generated implementation detail
        that ends up calling the right type's impl of the
        trait method Base::method.
        */
    }
}

In the code, x.method() is a call to this automatically generated method whose fully qualified name is <dyn Base as Base>::method. Since x was obtained by converting a BothTraits to dyn Base, the automatically generated implementation detail will wind up forwarding to <BothTraits as Base>::method which prints 1.

Hopefully it's clear from all of this that nothing here has anything to do with the unrelated trait method Derived::method defined by BothTraits. Especially notice that x.method() cannot be a call to Derived::method because x is of type dyn Base and there is no implementation of Derived for dyn Base.

static_dispatch(BothTraits)

At compile time we know that x.method() is a call to ::method. Type inference within generic functions in Rust happens independently of any concrete instantiation of the generic function i.e. before we know what T may be, other than the fact that it implements Base. Thus no inherent method on the concrete type T or any other trait method may affect what method x.method() is calling. By the time that T is decided, it has already been determined that x.method() is calling <T as Base>::method.

The generic function is instantiated with T equal to BothTraits so this is going to call <BothTraits as Base>::method which prints 1.

Did you find this article valuable?

Support Andrew_Ryan by becoming a sponsor. Any amount is appreciated!