Semantics of static and dynamic dispatch


I’m trying to understand all of the cases where static and dynamic dispatch occur. I know the basic cases described in the book with trait objects and regular generics (with/without trait bounds), but I wanted to ask about several different ways of declaring types and what the differences between them are.

Let’s say you have the following declarations. (The two commented out lines aren’t included since they won’t work because traits aren’t Sized.)

// Display is a trait in std
trait Display { /* ... */}

//fn a1(x: Display) {}
fn a2(x: &Display) {}
fn a3(x: &mut Display) {}
fn a4(x: Box<Display>) {}
//fn a5(x: Option<Display>) {}
fn a6(x: Option<&Display>) {}

fn b1<T>(x: T) {}
fn b2<T>(x: &T) {}
fn b3<T>(x: &mut T) {}
fn b4<T>(x: Box<T>) {}
fn b5<T>(x: Option<T>) {}
fn b6<T>(x: Option<&T>) {}

fn c1<T: Display>(x: T) {}
fn c2<T: Display>(x: &T) {}
fn c3<T: Display>(x: &mut T) {}
fn c4<T: Display>(x: Box<T>) {}
fn c5<T: Display>(x: Option<T>) {}
fn c6<T: Display>(x: Option<&T>) {}


  • What kind of dispatch is used for each of these functions?
  • What is the difference between the functions in the first group (a1-a6) and the functions in the last group (c1-c6)?
    Some of these might be obvious but are included anyway for the sake of completeness.
  • Does static dispatch mean that the compiler will always perform monomorphization?
  • Have I missed any cases?

  1. I believe dynamic dispatch will occur on all “a” functions and c4.
  2. in reference to the first question, “c” type functions are only expressing a trait bound, “a” functions type erase via. cast/coercion. This doesn’t force the T type to turn into a trait object in the compiled code; AFAIK the compiler knows about the concrete type in the global context and performs a static dispatch.
  3. IDK enough about compiler internals to answer this with confidence but I can assume so.
  4. Not that I can think of off the top of my head.


Calls to all can be with static dispatch. x.fmt() inside the functions is a different matter.
cx() are template functions. The compiler generates new functions from them each time a new type is used.
All cx() take exact types so all calls to fmt() are static.
For all ax() the functions take trait objects. Underlying type is not known at compile time so call is dynamic.
There are additional steps take before calling fmt() for each function. Handled in the machine code generated. (Referencing / Dereferencing)

fn d2<T: Display + ?Sized>(x: &T) { println!("{}",x)}

In this case T can be a trait object, in which case dynamic dispatch will be used. With regular type T it will be static.


No, not on c4, only a*.

*5 and *6 are superfluous - they are just special cases of *1 and *2 respectively.

Simple rules:

  • Whenever you see a type parameter you’re dealing with static dispatch on traits specified in constraint part of the signature.
  • When you see a trait nested behind a pointer indirection in the type position you are dealing with dynamic dispatch.
// Display is a trait in std
trait Display { /* ... */}

// dynamic
fn a2(x: &Display) {}
fn a3(x: &mut Display) {}
fn a4(x: Box<Display>) {}

// static
fn c1<T: Display>(x: T) {}
//          ^
//          |
//       constraint

Note that the same rules rule also govern trait impls:

// dynamic
impl<'a> for &'a Display { ... }
impl<'a> for &'a mut Display { ... }
impl Box<Display> { ... }

// static
impl<T: Display> Display for T { ... }

Note that the dyn syntax RFC is intended to make this clearer:

// dynamic
fn a2(x: &dyn Display) {}
fn a3(x: &mut dyn Display) {}
fn a4(x: Box<dyn Display>) {}

// static
fn c1<T: Display>(x: T) {}

EDIT: fixed silly typos in trait impl example - thanks @vitalyd!


You need to specify the T in the first 3 there. If T is ?Sized then it can be a trait object, which will use dynamic dispatch for trait objects. Otherwise it’ll be static dispatch.


Yes, although I’d phrase it as monomorphization leads to static dispatch. Note, however, the case @jonh mentioned:

This will get monomorphized but since a trait object can be passed, the dispatch to it will be dynamic.


Duhhh, that’s what I meant to write - fixed!