Dispatch
is the process of selecting which implementation of a polymorphic operation (method or function)
If that happens at runtime, we call it dynamic dispatch
whereas static dispatch
is fully resolved during compile time. In this article, we'll cover what's involved in dynamic dispatch.
dynamic dispatch
?why do we need dynamic dispatch?
It's because we want to have a variable that implements a certain trait(and its methods). As the trait is essentially unsized, if you want to dynamically dispatch over values, the Rust compiler must at least have something on a stack frame that points to the unsized values on the heap memory whereby the compiler can tell the size of the pointer. The easiest solution is putting &
. Imagine you want to initialize var
with anything that implements CustomTrait
as follows:
struct CustomStruct1;
struct CustomStruct2;
trait CustomTrait{}
impl CustomTrait for CustomStruct1{}
impl CustomTrait for CustomStruct2{}
fn main() {
// Here, we want to initialize var with anything that implements CustomTrait
let var: &mut dyn CustomTrait = if 1 == 1{
let mut custom_struct1 = CustomStruct1;
&mut custom_struct1
} else {
let mut custom_struct2 = CustomStruct2;
&mut custom_struct2
};
In the code above, I used trait object
that is essentially a reference to the trait with dyn
mark so it can hold both two different pointers - vptr
and data
. data
pointer addresses the data(of any type, say X)), and the vptr
points to the vtable
corresponding to the implementation of the trait for the type X.
When trying to run the code above, the compiler will complain saying “borrowed value does not live long enough.” It is because both CustomStruct1
and CustomStruct2
were initialized in the inner block and went out of scope right away. To work around this, we can use deferred conditional initialization:
fn main() {
let (mut custom_struct1, mut custom_struct2); // This makes sure that two variable live longer than `var`.
let var: &mut dyn CustomTrait= if 1 == 1{
custom_struct1 = CustomStruct1;
&mut custom_struct1
} else {
custom_struct2 = CustomStruct2;
&mut custom_struct2
};
}
But still, you will surely find it a bit verbose. And it turns out, using Box
smart pointer will give you a much more succinct solution:
fn main() {
let var: Box<dyn CustomTrait>= if 1 == 1{
Box::new(CustomStruct1)
} else {
Box::new(CustomStruct2)
};
}
Wait, why is it possible? because &dyn Trait
is not only a trait object
- it is just one of them. Listing off all of them, there are Box<dyn trait>
Rc<dyn trait>
, Arc<dyn trait>
, and more. So among them, using &dyn Trait
means that you have compiler take care of the lifetime which in this context is not necessary.