In this video, we'll learn what polymorphism is and how it
can help us better develop our object-oriented applications.
Polymorphism is a fundamental part of object-oriented programming.
There are several types of polymorphism in c++, and we've
already learned about two of them when we overloaded functions and
when we overloaded operators.
These are considered forms of compile-time polymorphism.
You might see the terms compile time, early binding and static binding.
These terms all mean the same thing.
They simply mean before the program executes.
In other words, something the compiler takes care of
way before the program runs.
Runtime, late binding and dynamic binding all refer to the association
of what the programmer wants to do with how to do it but at
runtime as the program executes.
We'll see some examples in a few slides that will make this very clear.
Polymorphism allows us to think more abstractly when we write programs.
We can think deposit or print or draw instead of thinking in specific
versions of deposit, print and draw.
So I can simply think deposit a 1000 dollars to this account.
And I don't have to worry about what kind of count it is.
I'll be sure that the correct deposit function will be called depending
on what type of account I have.
And this is all determined at Runtime.
In c++ the default type of binding is static binding.
This makes sense since c++ is all about efficiency and static
binding is done before runtime so programs can be as fast as possible.
However, sometimes we want to defer decisions until run time.
In c++ we can opt into this behavior by using base class pointers
or references and declaring our functions as virtual functions.
We'll see the details and syntax of this later in this section.
But first, let's look at the types of polymorphism that c++ provides so that
we can put this section in context.
Here's a simple chart that shows the types of polymorphism in c++.
There are two types of polymorphism, compile-time and runtime.
Compile-time polymorphism includes what we've already learned in
this course, function overloading and operator overloading.
We also have runtime polymorphism, and that's what this section
of the course will focus on.
We achieve runtime polymorphism by overriding functions and
using inheritance, using virtual functions and then having base
class pointers and references.
I think the best way to understand dynamic polymorphism
is to use a simple example.
First, let's look at a non-polymorphic example that uses static binding.
In this case, we have an account hierarchy using public
inheritance as you can see on the right side of the slide.
We'll assume that every type of account class has its own version
of the withdraw method and each withdraw method is different
depending on the type of account.
So let's create four objects, one for each type of account
and call their withdraw methods.
When we call account a's withdraw method, it will
call accounts withdraw.
This makes sense since a is an account object.
The compiler knows this and binds this method call at
compile-time or statically.
The same applies to objects b c and d.
In each case, the compiler is binding the calls to withdraw
based on the information that it has about the objects when they
were declared in the source code.
So b withdrawal calls savings withdraw, c withdraw calls
checking withdraw, and d withdraw calls trust withdraw.
So far so good.
It's what we've done and what we expect.
But now let's take a look at the pointer p.
P is a pointer to an account object.
So p contains the address of an account object.
Now we create a trust account dynamically on the heap and
assign its address to p.
Is this legal?
Sure, p can hold addresses of accounts and trust is an account.
The inheritance hierarchy tells us so.
So what happens when we call the withdrawal method for
the object pointed to by p?
Well, we're using static binding by default so the compiler doesn't
really know what type of account object p is pointing to at runtime.
It doesn't really care.
All it knows is that p is pointing to an account.
So it will call the account withdraw method.
This is probably not what we expected.
It certainly isn't what we wanted since we want the trust
object on the heap to use its own version of withdraw.
Please be sure that you understand this simple example
since it's fundamental to understanding dynamic polymorphism.
Remember, all the compiler knows is that p is a pointer to an account
so it will bind the withdraw method to the account classes
withdraw method at compile time.
Let's see another problem with static binding in the same context.
In this example, we have the same class hierarchy.
And let's suppose that each account class has its own version
of a display method that displays account information based on
the type of account we have.
So let's create a simple c++ function called display account, and it expects
a reference to an account object.
Since all our derived classes are accounts, we can pass any of
them into this function, and this function will call the display method
for the account object passed in.
So you can see that the code creates four objects: a b c and d, one for
each type of account and then it calls display account and passes in each
of these objects to the function.
The behavior you get from the display account function
may not be what you expect.
But by default c++ is doing exactly what it's supposed
to do, static binding.
When the compiler sees the call to acc.display in the display
account function, it will bind the call to accounts display.
So accounts display method will be called regardless of what
object was passed in, and our display will only display whatever
is in the account part of us.
There is a way for c++ to ask the account object being passed in,
hey, what kind of account are you.
And then depending on that, we can have if else statements that call
the appropriate display methods.
That's bad coding practice, and it also makes us program less
abstractly since then we've got to figure out what kind of object we've
got and then call its functions.
There's a better way.
That's where runtime polymorphism comes in.
Let's see how this same example works with runtime or dynamic polymorphism.
Now let's see the same example using polymorphic functions.
Here we see the same account class hierarchy except that this time we've
opted into runtime polymorphism.
We'll see how in a few videos, but notice that the withdraw
methods in the class diagram are now virtual functions.
This allows us to use runtime polymorphism when using base
class pointers or references.
Let's look at the same example as before.
Notice that the four count objects: a b c and d, have
exactly the same behavior.
Since we're not using base class pointers or references, these
examples are all statically bound just like before.
But now let's look at the pointer p.
In this case, when we call the withdraw method of the object
pointed to by p, the trust withdraw method will be called.
That's what we want and that's pretty cool.
The idea of using virtual functions tells the compiler not to bind the
calling compile time but instead defer the binding to runtime.
And at runtime, a check will occur to see exactly what p
is pointing to and then that object's method will be called.
In this case, the trust withdraw method.
Let's see the other example.
Again, now we have virtual display functions in our account class.
So whenever we use a pointer or a reference to a base
class, the binding is done at runtime, not at compile time.
So now we create four objects: a b c and d, each as a
different type of account.
Now when we pass these objects to the display account function,
the binding of acc.display will take place at runtime and call
the display method based on the type of object being passed in.
That's pretty cool and very, very powerful.
I can now write functions, methods and data structures that use
pointers and references to base class objects, and I know that the correct
methods will be called at runtime.
Remember, that account class hierarchy we did in the last
sections challenge, it used static binding since we hadn't learned
about dynamic polymorphism yet.
Remember, all those utility functions that we wrote, that
were all pretty much the same.
Now we could write just one version, and they'll work with
all the account types once we convert that account hierarchy
to use polymorphic functions.
Fantastic.
In the next video we'll go over how we can use base class
pointers in a little more detail.
Before we do the next video, let's go over here to the IDE
and walk through this problem.
This is the same problem we had with the account class.
And what I'm doing here I'm just creating a base and a derived class
to absolutely show you and walk you through what's going on here.
I'm in the section 16 workspace in the problem project so.
What have I got?
Well, you can see here it's can't get much simpler than this hierarchy.
I've got a base class right here, and it's got one public
method called say hello.
That's all.
And all it says is hello I'm a base class.
Then I've got a derived class, that's derived publicly from base.
And it's got the same method void say hello, but it says
hello I'm a derived class object.
That's it.
Then I've got this function right here that I've written.
This is not a member function, this is just a regular c++ function.
It's called greetings, and it expects a base object by reference.
Now remember, a derived is a base.
So I can pass in either a derived object or a base
object to this function.
And this function is simply going to say greetings, and then
it's going to call the specific objects say hello method.
So what do we expect here?
Well, the idea would be that if I have a derived object and i pass
it into this function right here, I'd say greetings and then hello
I'm a derived class object, but that's not what happens because
we're doing static binding.
So let's walk through this example.
It's really important that you understand the static binding
version before we start talking about the dynamic binding version.
Here's my main.
Let's create a base object, we'll call it b, and we'll
simply call b.say hello.
What do we expect here?
Exactly what you would think, right.
Let me just scroll up just a little bit, and I'm the compiler.
I'm looking in here.
There is no virtual -- we haven't really talked about virtual yet, but
you'll see the word virtual in here when we buy into dynamic polymorphism.
So the compiler sees this class hierarchy.
It doesn't see anything virtual so there's not going to be
any dynamic polymorphism here.
It knows that it needs to bind everything statically.
And also, we're not coming at this with a base pointer.
We have to have a base pointer, and we'll talk a little bit about base
pointer more in the next video.
So this is really simple for the compiler, right.
B.say hello okay.
Which say hello method do I bind?
This guy, right here.
How does it know?
Well, b is a base.
That's all the compiler knows is this right here.
So that's what it's using.
And that's just going to say hello I'm a base class object.
And let's do the same thing before we run it, we'll do it again, and we'll
do it this time for a derived object.
So let's do that, we'll say derived d.
And we'll say d.say hello again, same exact thing happens.
The compiler knows that d is a type of derived, right.
Its type is derived.
So in this case, it's going to bind this method right here.
Again, there's no pointers.
When there's no pointers and no virtual methods, everything is static.
So if we run this, what we expect is we expect this say hello to
say hi I'm a base class object and this say hello here on line 29 to
say hi I'm a derived class object.
So let's run that.
And there's the output, hello I'm a base class object and
hello I'm a derived class object, exactly as we expect.
Okay, so now let's run into some of the the issues that we'll run
into when we start using base class pointers and references.
Why don't we do this function first right here.
So suppose that I want to call that function.
Remember, this is a regular c++ function.
I'm going to call greetings, and I'll pass in the base class to it.
I can do that because b is a base.
And then I'm going to call it again.
And I'm going to pass in d.
I can do that because a derived is a base, right.
That's what my inheritance hierarchy is doing for me.
So what I expect here would be for the b to say greetings hello I'm
a bass and the d to say greetings hello I'm a derived, but that's
not what happens because we've got static binding going on.
So let's run this, and you'll see the problem.
There you go, you can see that right there those two
greetings is right here.
There's the output greetings, hello I'm a bass.
And then greetings, hello I'm a bass again even though I'm
passing or derived into it.
And we know now that the reason that this is happening is I'll
scroll up just a little bit is right here is the problem.
We're not using dynamic polymorphism.
And as far as the compiler is concerned, obj is
a reference to a base.
That's all it knows.
That's all it cares about.
So right here that will always bind to base say hello, and that's
why we're seeing that behavior.
Okay, the same thing happens when we've got a base pointer.
And let's do that really quickly.
So suppose we have a pointer, and we'll just call it ptr.
And in this case, ptr is a pointer to a base object,
right, an object of that type.
And we can say new derived.
Now this sometimes confuses students, but this makes perfect
sense, right, because think about it sometimes you think about the
hierarchy, you get all confused.
Think about what this really says.
This really says that ptr can hold the address of any base object, right.
Well derived is a base object.
A derived is a base that's what the inheritance is telling me.
So what happens here is here's ptr.
Here is my derived object that I'm creating on the heap, right.
And there's really nothing in there right now because there's
no attributes or anything, but this guy is pointing here, and
that's okay because this is a base.
So that's valid.
The compiler is happy about that.
But the problem comes in now when I try to call ptr say hello.
We've got no dynamic polymorphism, no polymorphic functions.
So what's going to happen is the compiler sees ptr
is a pointer to a base.
It's all it knows.
So it's going to bind this to base hello, always.
So we'll always bind that to base say hello.
And again, this is probably not what we want.
We want dynamic polymorphism to figure out what I'm pointing to at
runtime and call that appropriately.
So let's run this one last time.
And let's see if we get a run here, we do.
So here we go.
Let's walk through this one more time.
Here's b.say hello, hello on the base.
There's d.say hello, hello I'm a drive.
That's all using static binding.
That's perfectly fine.
Now here I'm passing in a b and a d to this method greetings.
In both cases, the compiler doesn't know anything except that
that's a base, so it's binding it to the say hello right here
on line 21 of the base class.
And then here's my pointer, and I'm calling say hello through the pointer.
Again, even though I'm pointing to a derived object
the compiler doesn't care.
This is not being deferred to runtime so the compiler is binding
it right now at compile time.
Okay, this also -- you don't have to use raw pointers for this.
We haven't talked about smart pointers yet but we will.
And suppose I include memory that's where the smart pointers live.
And I could scroll down here, and I could say something -- like let
me just close this up a little, I could say something like a std unique
pointer, and we can have a unique pointer to a base, and we can call
this ptr1, and then we could call make unique, which creates that pointer and
the pointer is a derived this time.
And we'll just empty constructor right here.
So that'll create a smart pointer this -- we'll talk about smart pointers later.
And then we could just say ptr1, and we can call the say
hello method there, right.
Now obviously we've got a pointer here so.
We really should free or delete that pointer just so we are
consistent with best practices here.
We don't have to delete smart pointers, they
take care of themselves.
So if we run this, you can see that even with a smart pointer, there's
a regular pointer, there's a smart pointer, they're both being statically
bound, just like you would expect.
All right, so in the next video, we'll talk about this base
class pointer and see a little bit more about how that works.