In this video, we'll learn about a specific type of virtual function
called a pure virtual function and how it lets us create abstract classes.
Before we learn about pure virtual functions, let's see what an abstract class is.
An abstract class is a class that cannot be instantiated.
So if we have an abstract class shape,
then we cannot directly instantiate shape objects.
You might be thinking so what good is a class like that.
Well, abstract classes are used as base classes in inheritance hierarchies.
And they're often referred to as abstract based classes.
So what do we call classes that you can instantiate from.
These classes are called concrete classes.
And those are the ones that we've been using all along in this course.
All of their member functions must be defined.
Examples of these classes might be a
checking account and a savings account in an account hierarchy,
a faculty and staff classes in the employee hierarchy
and enemy and level bosses in a player hierarchy.
An abstract based class is usually too generic
to be useful to directly derive objects from,
but it serves as a parent to be derived from,
and those could be concrete classes.
So in an account hierarchy, we're providing savings accounts,
checking accounts and trust accounts. But do we ever provide just an account?
Probably not. If a customer walked in and said they'd like to open an account,
they'd be asked what type of account they'd like to open.
So we would never really have account objects directly instantiated in our system.
We would have checking accounts, savings accounts or trust accounts.
So the account class can be thought of as a general class that's the parent
to all other classes.
The same can be said for the player class in a player hierarchy
and the employee class in a university personnel system.
In order for a class to be an abstract base class,
it must contain at least one pure virtual function.
So let's see what a pure virtual function is.
A pure virtual function is a virtual function that's used to make a class abstract.
We declare pure virtual functions
just like virtual functions, but we add the equal 0 after the prototype.
Typically, pure virtual functions don't have any implementation
since it's really up to the derived concrete classes to define this behavior,
but it is possible to give them an implementation.
In the code snippet in the slide, you can see that we declared a pure virtual function
named function.
Whatever class this function is declared in is now an abstract class
and cannot be instantiated.
So we see that a pure virtual function makes a class abstract,
which means we can't directly instantiate them.
But when we derive a class from an abstract class,
we must override the pure virtual functions in the base class in order to be a concrete class.
If we don't override all the pure virtual functions in the base class,
then the derived class will also be abstract.
This is very useful when it doesn't make sense for a base class to have an implementation,
but it does make sense in concrete classes.
For example, suppose we have a shape hierarchy.
The concept of a shape is too general or too abstract
to have shape objects in our system.
So we can define the draw method
as a pure virtual function in the shape class,
and that not only makes it an abstract class, but it also forces
all derived classes to implement the draw method if they want to be concrete classes.
And we can say the same thing for the defend method in the player class.
Here we see an example of a shape hierarchy.
Suppose we're creating a simple application that allows us to draw and manipulate shapes on the screen.
The shapes that we would be drawing and manipulating would be lines,
circles, squares and probably many others.
So we will have circle objects, line objects, square objects and so forth.
Would we ever have a shape object?
No, a shape object is just an abstraction that we can use as a base class.
If we declare two pure virtual functions draw and rotate
in that shape class, then all derived classes
must override these methods or they will also be abstract.
You can see in this class hierarchy that shape,
open shape and closed shape are all abstract classes
since their names are italicized and line,
circle and square are concrete classes.
Here is the partial code for the shape class.
Notice the declarations for the pure virtual functions.
It makes no sense to provide implementations for draw
and rotate in the shape class
since we have no idea how to draw or rotate the abstraction shape.
We'll leave that up to our concrete classes.
In this example, we derive a circle from a shape.
If this class overrides the draw and rotate virtual functions,
then it becomes a concrete class,
and we can instantiate circle objects.
In this case, we're overriding the pure virtual functions
so the circle class is a concrete class.
So remember, abstract base classes cannot be instantiated directly.
If we create a shape object shape, the compiler will give us an error.
Also if we create a shape object dynamically on the heap,
we will also get a compiler error.
But we can use base class pointers and references
as we've already learned to achieve dynamic polymorphism
and have some very powerful reusable and flexible class hierarchies.
Notice that ptr is a pointer to a shape,
and then we create a circle object on the heap and point to it from ptr.
Now we can call draw and rotate via the pointer.
How do we know this will work?
Well, it has to because all concrete classes derived from shape
must have overridden the draw and rotate methods.
That's pretty awesome.
Let's head over to the IDE and see an example that uses pure virtual functions
and abstract base classes.
So I'm in the IDE. I'm in the section 16 workspace in the pure virtual functions project.
What I've done here is I've created a shape hierarchy,
and I'm using pure virtual functions to establish an abstract base class.
So let me walk you through this and then what we'll do is we'll run some examples
when we write a main, and we'll create some shape objects
and call some of the methods so that you can see exactly how this works.
Okay. So here's my shape class.
Notice that the shape class has
these two pure virtual functions: one is called draw, one is called rotate.
That makes this class shape an abstract class.
In this case, it's going to be an abstract base class because I'm going to derive from it.
I cannot instantiate shape objects. I can't say
shape s.
Okay. I can't say -- I can't even go at it through a pointer.I can't say
pointer p equals new shape.
Okay. Right there I'm trying to create a shape object directly, I can't do that.
So both of these will give me compiler errors.
An error will say something like you can't instantiate an abstract class or something like that,
which which is a good error, makes sense.
So that's my abstract base class.
Notice the virtual destructor, always use that when you've got virtual functions.
Perfect. So now let's create an open shape.
An open shape would be a line, a spline things like that and closed shapes would be circles
and squares and so forth. So here we're creating a class called open shape.
Notice it's derived publicly from shape.
Also notice that I did not override those two virtual functions.
So that makes this class abstract. I cannot instantiate
open shape objects.
Same idea here. Here I've got a closed shape, that's derived publicly from shape.
I am not overriding those two methods that you can see in the base class in here.
So this class, again, is an abstract class.
Okay. So now let's look at our concrete classes. Those are the ones that obviously know how to do things.
It makes no sense for me to write code here
that draws a shape or rotates a shape, I have no idea what a shape is,
right. This is just an abstraction for a shape.
Rotating a square versus a line is very different,
and drawing a line versus a square is very different.
So the concrete classes are the ones that take care of that.
Okay. So let me scroll down just a bit to be the concrete
glasses right here.
So these are my concrete classes right here. You can see I've got a line,
I've got the circle,
and I've got the square, right here.
So in this case, I'm not deriving directly from shape.
Line is an open shape, so I'm taking advantage of that.
It's a concrete class this time because you can see right here
I'm overriding both of those pure virtual functions,
that makes me a concrete class. So now I can have line objects,
same with circle. You can see here circle is a closed shape.
And right here, I'm overriding those two pure virtual functions.
And then down at the bottom, you can see exactly the same thing with the square.
Right here, you can see that square is a closed shape,
and I'm overriding those two pure virtual functions.
So I can have square objects out there.
Okay. So this is the same hierarchy that you saw in the slides that were displayed in the uml diagram.
Now let's write some code that will allow us to use these.
Okay. So let's write some code in the main.
We can start off by just trying to create a shape object, right.
We can create a shape object s
or we can come at it with a pointer, just like we did a little bit ago.
We'll just call it p is a new shape. And you can see that if I try to compile this,
it's not going to compile.
There's my compiler error.
It says cannot declare variable s to be of an abstract type shape.
And same thing here with this new shape.
I'm not allowed to instantiate shape objects.
Okay. So we've seen that before.
And that's a function of those pure virtual functions.
The fact that they're in there makes the classes abstract.
I'll comment these two lines of code out.
And if we also try to
instantiate open shapes and close shapes, we'd also get the same error
because they are also abstract classes.
Now suppose we create a circle, right, we can certainly do that. We can say circle c,
and we can call c.draw.
We know it's there. Now in this case --
so in this case, you can see it says drawing a circle, right.
But this is not coming at it with a base class pointer.
This is all being statically bound. We're not using dynamic polymorphism here.
And that's okay, sometimes you want to do that.
But the real purpose of creating that abstract base class is so we can come at this
class hierarchy with base class pointers or references.
So let me comment that code out as well.
And let's create a base class pointer.
So the base class in this example is shape, right.
So we'll create a shape pointer, and we'll just call it ptr.
And what we'll do is remember, I can't say new shape here,
but I can certainly instantiate derived concrete classes.
So what I'll do is I'll create a circle.
And now if I do ptr draw or ptr rotate,
so you can see now that if I call the draw and rotate methods through the base class pointer,
what's going to happen is we're having dynamic polymorphism.
So the function call will be bound at runtime,
and we're going to call the draw on the rotate for the circle because that's what I'm pointing to so.
If we run this, we can see that we're drawing and rotating the circle,
which is pretty cool.
Okay. So this is exactly what we've been doing before. The only difference here is that we're using those
pure virtual functions to define an interface,
if you will, at that base class level.
And all our concrete classes must implement those pure virtual functions.
Otherwise, they are abstract themselves.
Okay. So let's look at a couple of other examples.
And let me comment this out again real quick so it doesn't clutter up our display.
And let's just create a few shape objects.
So let's say let's create a few shape objects through pointers.
So let's say s1 is a new circle,
S2 is a new line,
and finally, we'll just say s3
is a new square.
Okay. Now remember, we've got all shape pointers here, right,
all base class pointers.
And what we're actually pointing to are our concrete derived classes.
So that's pretty cool. Now what we can do is,
let me write the code here to delete these guys down at the bottom, so we definitely don't want to leak --
Okay. So I've written the code to free up that allocated storage down at the bottom.
And now we're free to use these guys.
And now we're free to use these guys. So what do we want to do.
Well, why don't we put them in a vector, and then loop through the vector and
call each one of their shape methods that say the draw method.
So let's create a vector.
And what is that vector -- what's my template parameter there.
Well, it has to be a shape pointer, right.
It can't be a shape coming at it from a base class pointer.
So in this case, I have a shape pointer.
And again, I'm using a raw pointer here in the shape so. You've got to be really careful when you do that.
In the next section, we'll learn about smart pointers. You should probably be using a unique pointer here
or a shared pointer.But in this case,
this will be -- this will work just fine. So there's my shapes.
And let's set it to s1, s2 and s3.
So there's my vector. It contains pointers to shapes.
And I've got all three of them in there.
And now what do we do. Well, let's loop through that vector, right.
So we'll use a range base for loop. And we'll say for, let's say, const
auto p,
p is going to be the pointer to the shape in shapes.
And when I'm here, I can just simply call p's draw.
That's it. Simple as that.
If I run this now, I should get three lines of output:
one for each of those objects. So in this case, I'm calling drawing a circle,
drawing a line and drawing a square.
So you can see that's pretty cool.
Imagine you have a collection of base class pointers
and they're all pointing to objects on the screen, right.
All of those objects are going to be concrete objects.
And then let's say you want to refresh your screen, what do you do? Well, you just loop
through that collection and say
draw draw draw draw draw for each one of those objects, right.
And each one of them knows how to draw itself.
Makes it super easy to think abstractly and think in the general.
So why don't we actually do that.
Why don't we write a function, just a basic function right here.We'll call it,
let's say it's, void. Let's call it screen refresh.
Okay. So it's called screen refresh. And what are we going to do what
we're just going to pass that vector into here and let the vector do its thing.
So const -- again, it's a std vector
of shape base class pointers.
And we'll pass it by reference so we don't copy it.
That's it, right. That's pretty cool.
So now I'm going to say std cout.
I'll just say refreshing, just so we know it's being called.
And now we'll just loop through that vector, right. So I'm going to say 4,
again, same thing we did before, for const.
We'll use auto p again,
and shapes is my collection.
And right here all I'm doing is p draw.
That's awesome. I mean think about the power that we have here, right. Remember, we're coming at this
with an a vector of base class pointers.
So we know we've got dynamic polymorphism
kicking in.
We know that each one of those derived classes that are concrete
must have implemented draw.
So i can simply call this without even worrying about what I'm pointing
to because I know it'll be bound correctly at runtime.
And all we need to do now is let's comment this loop out here,
and let's just call that.
We'll call screen refresh, and we'll pass in the shapes vector.
And there you go.
Refreshing drawing a line, circle drawing a line, drawing a square. That's pretty cool.
So you can see the real power here.
And again, the real power of the pure virtual functions is
to really force all your derived concrete classes to implement those methods.
Now you know that they must be implemented,
so you can create base class pointers and just
call any of those guys. And you know that they're there, they have to be there.