We've already seen that in order for the c++ compiler
to use dynamic binding of method calls,
we must have an inheritance hierarchy,
we must be using a base class pointer or base class reference,
and we must declare the methods we want dynamically bound as virtual.
In this video, we'll look at the base pointer requirement
and see why it's so useful and so powerful.
We'll use the account class hierarchy on the right side of the slide as our example.
Let's also assume that this class hierarchy is now using dynamic polymorphism.
We'll learn how to do that in this section of the course.
For now, let's just concentrate on the effects and the power it gives us.
So we'll create four pointers to account objects,
and we'll initialize each one to a different type of account created on the heap.
So as you can see, p1 holds the address of an account,
object, p2 holds the address of a savings account object,
p3 holds the address of a checking account object,
and p4 holds the address of a trust account object.
We've already seen that this is valid semantically
since they're all accounts due to the public is a inheritance we're using.
Now we can call the withdraw method using the base class pointers,
and c++ will figure out which method to bind at runtime
based on the type of the object being pointed to by each pointer.
This is pretty cool. There's nothing else you need to do once the hierarchy is set up.
Of course, we need to delete the storage allocated by the pointers when we're done with them.
Okay. So now let's see a more compelling use case for using base class pointers.
In this case, we have the same four pointers we created in the previous slide.
But I've declared an array that holds pointers to account objects.
These will be the base class pointers.
I then initialize the array to hold the four pointers we declared previously.
Now I can simply loop through the array
and call the withdraw method for each element in the array,
and the correct withdraw method will be called
based on the type of the object that each pointer points to.
You can see how powerful this is.
It doesn't matter how many base class pointers I initialize the array with
or what type of accounts they're pointing to, it will work as expected.
Also, if we replace one array element for another, it will also work as expected.
This is what i was talking about when I said programming more abstractly or more generally.
Here I'm simply thinking
call the withdraw method for each account in the array.
That's it. No more details required.
Of course, this also works with other collections such as a vector.
Here we have the same four pointers,
and then we create a vector of pointers to account.
So the vector holds base class pointers.
We can then initialize the vector to p1 p2 p3 and p4.
Then we can simply loop through the vector using a range-based for loop or an iterator
and call the withdraw method for each vector element.
Think about what would happen if we added another class to our account hierarchy,
say a bond account.
None of the code that we have that works with account objects needs to be changed.
Since a bond account is an account, it will automatically work with our existing code.
A little note here though,
be careful when you're using raw pointers and collections such as vectors.
It's better to use smart pointers in this type of example.
But we haven't really learned about them yet, we will soon. So there you go.
I hope you can see the power of the base pointer
and how much more abstractly we can think when writing our code.
Let's head over to the IDE, and we'll run these examples so you can see firsthand.
Okay. So I'm in the IDE.
I'm in the section 16 workspace in the BaseClassPointers project.
Now let me explain what's going on this project. I've already gone ahead
and taken that account hierarchy account,
checking account, savings account and trust account, and I've made it
so that it works with dynamic polymorphism.
So I've done the virtual functions, I've done all that already.
I just want to show you how to use this and see how powerful it is. In the next video,
we'll learn about virtual functions, and we'll start implementing it ourselves.
But let's take a look at the main. This is really what the point of this video is.
If you look at the main, you can see
that first we're going to do the pointers, and we'll deal with arrays and vectors.
But let's just do plain pointers first.
Here you can see that I've got the four pointers:
p1 p2 p3 and p4,
each one points to a different type of account,
but they are all accounts, right, because we've got public inheritance in our hierarchy.
And then what we're doing is we're calling the withdraw method for each one of those objects.
And the idea is if we've done our dynamic polymorphism correct,
p1 will call the account withdraw,
p2 will call the savings withdraw,
p3 will call the checking withdraw, and p4 will call the trust withdraw.
Okay. So let's give this a build and run.
And you can see the output right here. I'll just move it over so we can see it, we can see pointers.
And then the withdraw methods are called right down here. You can see
that we're calling the accounts withdraw,
then the savings withdraw, then the checking is withdraw, and then finally, the trust withdraw.
So exactly like what we expected.
We've got binding now happening at runtime. That's really, really powerful stuff.
We also have the cleanup here. And I'll show you what that looks like. That's just deleting those four pointers,
so we free up the storage that we allocated for them.
All right. So now let's take a look at how this would work with arrays.
And I'll uncomment this out.
And in this case, I'm just printing out a line that says array, so we know what's going on.
And what I'm doing here I'm creating an array, and this is the declaration right here.
I'm creating an array that's what the brackets say
of pointers to accounts. So I'm going to create an array, in this case,
and it's going to have four pointers in it.
Each one of these pointers is going to point to an account object.
So that's what it looks like. P1 here,
right, p1 is the account. So this will be an account.
P2 will be a savings account. You get the idea, right.
That's a checking account, and that would be a trust account.
And then what I'm doing is I'm simply looping through this array right here.
And I'm calling the withdraw method for each one of those pointers.
So this will call the account,
this will call the savings, this will call the checking, and this will call the trust withdraw methods.
Okay. So the real powerful piece here is that you as you can see,
I don't have to worry about that in the loop.
All I'm doing here is simply calling the withdraw method for
each one of those guys, and whatever happens to be there
will be dynamically bound
and it'll call the correct method which is super, super powerful.
So let's give this one a run.
And
we can see that here's the run.
And you can see the array right here.
And notice what's happening here, i put p1 p2 p3 and p4 in the array,
those are base class pointers, I'm not putting account objects in the array,
I'm putting pointers to account objects in the array,
and they all happen to be base class pointers, that's why this works.
So in this case, I'm calling this four times right, 0 1 2 and 3.
And you can see here in account, in savings, in checking and in trust.
So we're getting the exact behavior that we want.
So let's modify this just slightly. And what we can do is we can take --
and I'll just copy this again, so we can see that output one more time
so we can separate it and keep it nice and clean.
What i want to do here is I simply want to say let's set array
sub-0 that first item in the array,
let's set that to p4.
All right. P4 is a pointer to a trust account.
And then let's just loop again, and call the withdraw method for all of those guys.
Now our output should be a little different now, right. Because our first array element
that one at index 0 is no longer an account, like it was up here.
We put p1 in there,
now it's a trust account.
So what we expect in the output now would be trust, savings, checking, trust, right.
So let's try that out.
And let's see what we get here: trust, savings, checking, trust. You see
that's exactly what we want. It really doesn't matter what these pointers point to,
it will get bound correctly.
And the real powerful thing here is I never really changed that loop, right.
The loop is exactly the same as it was before.
I didn't have to do any checking for anything.
All right. So now let's take a look at the vector version.
And as I mentioned in the slides, be really, really careful when you use raw pointers and
any kind of collection but especially a vector.
What you want to do is if you're going to use them, use them this way
where you create -- you initialize the vector this way. Don't put
like -- something like new account or something in here
because that could be problematic when it comes to
destroying those objects. You'd have to loop through the vector and destroy them yourself.
But anyway, we'll just go look at this example real quick. So here's my vector.
What am I doing. Well, in this case I'm creating a vector.
And what is the vector of, right. My template parameter is a pointer to an account.
So what have I got, just like we had an array, right. We have a vector
of pointers to accounts. That's my base class pointer.
I'm initializing it to p1 p2 p3 p4.
And at this point all I'm doing is a range-based for loop right here,
and I'm just using the auto keyword, it's going to figure out that that's an account pointer.
And I'm looping through there. And
that's it. I'm just calling the withdraw method for
each one of those pointers that's in the vector.
Again, what we expect here because we've got p1 p2 p3 p4, we expect
account.
Let me go back up and make sure I get this right.
We expect account, savings, checking, trust. That's the order we're putting them in.
So when this runs, that's what we expect. Let's give this a run.
And there's our vector case right here.
And in here, we've got account, savings, checking, trust, exactly what we wanted.
All right. So let's just do one more little quick change here.
And what we can do is let's add two more items to that vector.
And what we can do is we can add that trust pointer again. We can add it two times at the back.
So why don't we do that.
Let me copy this again just so we have some separation in the output,
and we can better follow it.
So what we'll do here is we'll just say my vector is called accounts.
So I'm going to say accounts.push_back.
And let's push back p4, so we're adding p4 at the end.
And let's do it again,
and we'll add p4 again.
So now when we loop through that vector this time,
we expect those first four output statements to be the same,
but we expect two more output statements at the end
for the trust withdraw being called, right,
because now we've got 6 items in this vector. So let's give that a run.
And here we go. You can see 1 2 3 4 5 6
items in the vector.
We're calling the withdraw for each one of them, and you can see how the last three are trust, right.
They give -- the fourth one was one that was already in there and then the two that I just added right here.
As I said, be really careful when you're using raw pointers in these things it's
better to use smart pointers. We'll talk about smart pointers in the next section.
We could have used a shared pointer here, even a unique pointer,
which would make things a lot more safe.
All right. So there you go. Hopefully, you can see the power here.
Remember, if I come up here,
and I've got my accounts. And I decide to add a new account here, some
like I said a a bond account or something, any kind of account.
As long as I tie it into this hierarchy
and make sure that the methods that are being called are virtual there as well,
then I can create an account p -- account object right here p5,
that's a bond account. And all this will work, right. All these loops,
everything will work exactly the same. I won't have to modify any code
because that's the point. I can think generally,
I can think abstractly.
If you think of this vector right here, this code right here, what does this say.
It's basically saying loop through the vector and call the withdraw for everything in the vector.
That's pretty high-level thinking. That's pretty powerful.
If I've got a drawing program that's drawing shapes, I can just
loop through all my shapes and ask each shape to draw itself
or move itself or transform itself.
So I'm really able to think abstractly and not get bogged down with details.
And that's the power of dynamic polymorphism.