[No Audio]
When code involves generics, there needs to
be a mechanism to determine which specific
version is actually executed. For instance, a
generic may assume a float value or an i32
value, the compiler needs to somehow know
what particular concrete types it will be
converted to during the execution. This is
called dispatch. There are two major forms of
dispatch, static dispatch and dynamic
dispatch. While Rust favors static
dispatch, it also supports dynamic dispatch
through a mechanism called trait objects.
Let's start with a simple example and things
will start to make some sense. Let us define
a simple trait called print which will be
used to print different types of values.
[No Audio]
This print will contain a single function
called trait, which will accept a self type.
[No Audio]
Let us now implement this trait for a string value.
[No Audio]
The signature of the function should match
the signature used inside the trait.
[No Audio]
Inside the function I will include a single
statement where I will display the value of the string.
[No Audio]
Next, I will implement the same trait for an i32 value.
[No Audio]
Again, the signature of the function will
match, to the signature inside the trait.
[No Audio]
Finally, we will include a print statement
for displaying the contents of the value received.
received.
I will next define a simple display function
with a generic T having a trait of print.
received.
This means now that T can be anything which
has the print trait. In this case since print
rates are only defined for string and i32
values therefore, only these two types will
be allowed inside the function I will call
the print function on x.
[No Audio]
Let us now called the display function from
the main using an i32 value and a string value.
[No Audio]
Now, what happened behind the scenes is that
Rust uses something called as monomorphization.
This means that Rust will
create a special version of display for both
i32 and string values, it will next
replace the call sites with calls to these
specialized functions. Let me write the code
which more or less the Rust will generate.
[No Audio]
The first call will generate the specific
version of the function with the type of T,
being set to that of i32 and the second
call will generate the specific version of
the function with the type of T being set to
that of the string type, this is called monomorphization.
With monomorphization, the
Rust performs a static dispatch which enables
it to determine which specific versions are
actually being executed. And in particular,
the static dispatch enables the Rust resolves
to a specific concrete type for generic T at
compile time, the compiler creates a
different versions of this function for each
concrete type used to call it. T will
represent a known size type after
compilation. This, that is its size is known
at compilation time. The static dispatch
allows for inlining, and hence usually has
higher performance. By inlining we mean the
replacement of a function call at a call site
with the body of the function. It however
have some downsides, it generates many copies
of the same function existing in the binary,
one for each type. However, please note that
it is a sort of an in demand in the sense
that not all allowed versions will be
created, but rather those versions are
created, which are actually being used. In
contrast to static dispatch there is another
approach called dynamic dispatch. The static
dispatch may sometimes become inefficient
when it tends to make many copies of the same
function. Rust provides dynamic dispatch
through a feature called a trait objects.
Simply put, a trait object is a reference to a
trait. They are normal values that store a
value of any type that implements the given
trait, where the precise type can only be known
at runtime. Since the type is not known at
compile time, therefore, the specific
versions are not been created. Let us see,
let's see how to define a trait object
with the help of an example, I will comment
out the code first.
[No Audio]
Let us consider a couple
of simple structs called Person and Students
with a field of name.
[No Audio]
Next, we will define a trait called info,
which will contain a single function of info.
[No Audio]
First we will implement the info trait for the Person.
[No Audio]
Inside the implementation, we
will define the info function which will
display the name.
[No Audio]
Next, we will implement the
same trait for the student and will display
his name inside the info function.
[No Audio]
Next, to clearly see the difference of static
and dynamic dispatch, I will define a couple
of functions, the first function will be
called a static_dispatch.
[No Audio]
The input to the function is a reference to a generic
type T, where T can be anything that implements info.
[No Audio]
Next, I'm going to define
another function called dynamic_dispatch.
The input to this function will be
a reference to the trait info.
[No Audio]
This reference to a trait is called a trait
object which is being specified using the
keyword of dyn. A trait object is defined by
having a reference to a trait. We will explain
it in a second, but for now, let us complete
the function by calling the info function
inside the function.
[No Audio]
Okay, now, let us explain it in some detail.
We know the type has traits, that is types are
tied with some traits and traits are not tied
with types. They rather define some
functionality which may be followed by some
types. Therefore, when a trait object is seen
by Rust, it does not try to convert it to
specific concrete types. Since that trait
does not have types, and therefore the
specific concrete versions are not being
created at compile time. The end followed by
dyn and then the name of the trait is
basically the trait object. In this case.
There's only one version of this function at
runtime. So this reduces the size of the
compiled program if the function is called
with several different types versus using
stating_dispatch. We can create
the trait object using different pointers
such as Box or Rc or Arc smart pointers by
wrapping the info trait inside them with the
dyn keyword, they are all more or less
equivalent. And now let us use these two
functions inside the main. First I will create
a variable of type Person with a suitable name.
[No Audio]
Next, I will create another variable of type
student with another suitable name.
[No Audio]
Let us now call the static_dispatch for
the two variables.
[No Audio]
In the same way, I will call the dynamic_dispatch
for the two variables.
[No Audio]
The static dispatch function can be
called for any type that has the info trait.
In the same way that dynamic dispatch can
also be called for any type that has the info
trait. The key difference is the input types
to the two types of dispatches. The
function with static_dispatch
receives a generic type which is resolved
into concrete types. While the dynamic
dispatch function has straight objects. In
particular, it obtains a trait object plus a
table with all the methods that are defined
on the trait. Calling a specific function on
the object will mean to look up, to look it up
in the table and then executing the code.
This will take some time to look and identify
and then retrieve the code of the function.
In contrast, static dispatch does not involve
this lookup and is therefore comparatively
faster. Now let's cover a nice use case of
the trait objects or dynamic dispatch.
Earlier in this course, we covered an example
where we pointed out a solution to a key
limitation of vectors, which is that they
allow only elements of a single type. We
created a workaround, a workaround where we
defined an enum which having two variants for
holding integer and float types. Now,
defining a vector of the enum type allows us
to store different types of data in the same
vector. This is perfectly good solution when
our different types are off limits.
accountable types that we know when our code
is being compiled. However, sometimes we
encounter a situation where our types may
increase with the passage of time and
therefore, we need to keep room for those
types which may evolve later on with respect
to time. This means we need to have some
flexibility in our code, so that it can
easily adapt to new evolving types. The
dynamic display provides a perfect solution
in this regards, I will comment out the code
and we'll start fresh again.
[No Audio]
I will use the same program that we wrote earlier.
So let me paste it.
[No Audio]
This program was based on a print
trait which contains a print function, we
have two types of i32 and string which
implements this trait. Finally, we have a
display function, which which accepts any
type which implements the print trait and is
used to call the print function. Now, from
the previous discussion, we know that this
function is doing a static dispatch.
So, let me rename it.
[No Audio]
In the main I will change the
name of the function accordingly in the call
to the function.
[No Audio]
Now, what I want is to pass
a vector containing different types to the
function and get the values to be displayed.
Since the generic can assume a single type
therefore, the static dispatch will not work
here. This is because the generic will be
resolved to a single concrete type during the
compilation, this can be done using the
dynamic dispatch, I will define another
display function which will essentially do
the same but using the dynamic dispatch.
[No Audio]
In the input to the function, I will mention a
vector of trait objects since I want to pass
in a vector containing different types to this function.
[No Audio]
This now means that x is a vector of any
types that has the trait print. In the body of
the function I will iterate through all the
values of the vector x, and will display its values.
[No Audio]
In the main now, I can call this function
with a vector containing different types.
[No Audio]
Please note that we need to pass in
references to values and not concrete values.
Let us cargo run this.
[No Audio]
You may note that the compiler
has no issues. This is one of the
main use cases of trait objects, that is, it
allows for defining vectors of having
different values, with the possibility of
extending the types inside the vector in
future. Earlier we mentioned that a trait
objects are reference to traits and they can
be created using either a simple reference or
Box an Arc or Rc smart pointers. So let us
redefine the function display_dynamic
using the Box smart pointer.
I will write the dyn print inside a Box and we'll
delete the simple reference.
[No Audio]
In the call to the function in the main program,
I will write the values of the vector inside the Box,
and we'll delete the references.
[No Audio]
You may note that this compiles fine
in the same way we can use
the Arc or Rc smart pointers to create the
trait objects. Okay, in summary, it is more
efficient to use static dispatch when the
underlying types are not many numbers. The
standard library also generally tries to
statically dispatched wherever possible.
However, when the types involved are many, we
would like to do a dynamic dispatch. The
static dispatch is made possible with the
help of the monomorphization, while the
dynamic dispatch is made possible with the
help of trait objects. With this we
end this tutorial see you again with
more, and until next tutorial happy Rust programming.
[No Audio]