In this video, we'll see how we can use abstract classes
as interfaces in c++.
An interface class in c++
is a class that has only pure virtual functions.
These functions provide a general set of services that users of the class can use.
In order to be useful, these pure virtual functions must be declared as public.
Any class that wants to be a concrete class and support the interface
provided by the interface class
can override the pure virtual functions and implement these services as needed.
Remember, every service must be implemented,
and c++ will strictly enforce type information.
So when overriding the function, prototypes must match exactly.
Unlike java, c#, and other languages,
c++ does not provide a specific keyword
or way to create an interface as part of the c++ core language.
So in c++, we use abstract classes with pure virtual functions
to achieve the concept of an interface.
Suppose we want to be able to provide any user-defined object,
the ability to be printed on a stream like cout,
we can do this by creating a printable interface class
that provides the service in terms of a pure virtual function.
And then our user-defined classes can simply be derived from printable
and override this pure virtual function.
Let's see an example.
Here you can see that we have a class name printable,
and it has a pure virtual function named print.
This makes this class an abstract class.
The printable class also adds a friend function
that we'll use to allow us to print any type of object that conforms to this interface.
You can see that the overloaded operator function
You can see that the overloaded operator function at the bottom of the screen,
and it calls obj.print.
This would dynamically bind to the print function of whatever object type was
passed in since we know that that object is printable and we have a reference to it.
That's pretty powerful stuff.
At the end of these slides, we'll head over to the IDE, and I'll show you how we can use this interface
with the account hierarchy as well as with non-account classes.
So for any class to be printable,
it must be derived from printable,
and it must override the pure virtual function print that's in the printable interface.
That's it. So in this case, you can see that any class is derived from printable
and it does override the print pure virtual function.
Now any class is a printable.
So let's create some any class objects and print them.
First, we create ptr as a pointer20 class and create an any class object.
Then we dereference the pointer to get that object,
and we use it in an output statement.
This will call the overloaded insertion operator,
which will, in turn, call the function print of any class,
and this will display "hi from any class".
Now suppose we have two functions, function1 and function2.
Function1 expects an any class object by reference,
and function2 expects a printable object by reference.
Well, we can call both these functions and pass in the object ptr is pointing to,
why, because that object is an instance of any class
and because that object is a printable.
In both cases, we get the same output.
The print function in any class is bound dynamically.
Let's see another example. Here we have a shape class that only has pure virtual functions.
So it's an abstract class, and it can also be used as an interface class.
What we're saying is if you want to be a concrete shape,
then you must be derived from shape and you must override the draw and the
rotate pure virtual functions. So let's do that.
Here we declare a circle class derived from the shape class.
We know that if we want a circle to be a concrete class,
then we must override draw and rotate,
and that's exactly what we're doing in this example.
Sometimes you'll see classes that are intended to be used as interface classes
named with a capital I preceding the class name.
This makes it easy to know just by the name that the class is intended to be used as an interface.
As you can see in this example, shape was renamed to I shape.
Now when you see circle being derived from I shape,
you know that I shape is intended as an interface class.
This isn't a requirement but you tend to see that out there.
So we can now use pointers to I shape objects to use dynamic polymorphism.
Here we create a vector of pointers to I shape objects,
and then we create three pointers and initialize them to point to three different types of shapes.
In order to call the overridden functions in the concrete classes,
we simply loop through the vector and call the functions.
You might be thinking isn't this the same as we did before?
Yes, since we must use inheritance
and pure virtual functions as interfaces in c++.
But there's another difference, and I'll show you that example in live code.
Let's head over to the IDE now and we'll see.
Okay so I'm back in the IDE. I'm in the section 16 workspace in the interfaces
-start project.
We're going to do eventually is to implement that printable interface,
so that we're able to print any kind of class objects
on a stream, any kind of class objects that conform to the printable interface.
So let's use this account hierarchy as a starting point.
This is similar to what we did in the previous sections challenge.
I've gotten rid of a lot of the other stuff that's not necessary,
but let's just look at the key elements here.
Here we've got a class account, right here.
And you can see that we have a friend function
that overloads the insertion operator, it expects an output stream
and an account object by reference, perfect.
Now we're going to implement that, and we're going to implement that right here.
You can see that it's obviously a friend, it's not a member of the class,
and it expects an account.
So if we have an account object and we put it on a stream,
this should fire and print it out, and that absolutely works.
And all it does is just says account display.
Okay. So now let's look at checking account.
You can see the checking account is doing the same thing.
We do we're duplicating this code all over the place,
and it's right here. There's my checking account. It is an account.
Well, I've got to do this now with a checking here.
So I'm implementing it over here. So this definitely works with the checking account,
and it displays checking display.
The reason that we have to do this is because
a friend functions are not inherited.
So even though I've got this guy up here, I'm not getting them down here
or further down the hierarchy.
So that's one issue.
The other issue is that in order for us to implement this operator, the insertion operator,
it really doesn't make sense for it to be a member function.
Remember, if it was a member function, I'd need the object on the left,
followed by insertion operator and then the stream.
Well, that's not how we do it, right. We're doing something like stream object.
So this really would work for sure, but it would feel really clunky, and nobody would ever use it.
So we've run into a problem now.
We're doing the same for the savings account class right here
and the trust account class right here.
So that's all we're doing. We're just
creating these four classes, overloading the insertion operator for each one of them,
and each one of them will display savings display, trust display and so forth.
So we can have a simple main, and this is what main looks like right here.
Give me an account a, put it on the stream. Give me an account c,
same thing, same thing. You get the idea, a c s t.
We should see account display, checking display, savings display, trust display.
So let's run that and, be sure this works.
And that's what we get. We've got our four objects:
the account, the checking, the savings, the trust, everything works like it should,
but it really doesn't. There's one problem here.
And let me copy some code that I've already written in here just to save a little time.
So here's the code we've got now.
We've got p1 is a pointer to an account, right. That's a base class pointer.
And I'm creating an account and pointing to it from p1.
And then I'm going to put
the object, right. So I need to dereference p1 because p1 is a pointer.
So I want the object it's pointing to and I want to put that on the stream.
This will call the overloaded operator for the account.
Now I want to do the same thing with p2.
Except p2 this time is actually pointing to a checking account.
Although again, we're coming at it through a base class pointer.
And I want to dereference p2 and put it on the stream.
So what we expect is to say account display, checking display,
but that's not what happens. And let's see what happens.
You see what happens is account display, account display.
Why does this happen? Well, it happens because
we've got no virtual functions going on right. The only virtual functions we've got are --
let me close this down so you can see it exactly. Let's go up to my account class.
And the only virtual functions we've got are
withdraw, right. That's it. That's the only virtual function we have.
We have no virtual function that's going to allow us to work with
any kind of account object in the context of this operator here,
and it's really clunky to do so because of the syntax.
So what we could do is
we could certainly add some kind of print function to this account class
and it's inherited all the way down and make it virtual.
And that's kind of what we'll do.
But what we'll do is we'll create a separate interface class
rather than put all the stuff in account.
And what that buys us is that not only am I able to use that
printable interface class that we're going to create
with this account hierarchy, I can use it with any kind of class,
not just these account classes.
So let's do that. What I'm going to do is I'm going to switch over to the interfaces
dash complete project.
Right now it looks just like this one, but what we'll do is we'll modify
it and get this working with the printable interface.
Okay. So I'm back in the interfaces-complete project.
And you can see that right now it's exactly what we did earlier.
I haven't changed this code at all. So this is exactly what was in interfaces-start.
So what we want to do is we want to create an interface class called printable
or i printable.
We want you to find a virtual function in there.
And then we just want to extend this so it makes more sense and it works better.
So first thing we're going to do is we're going to clean up this mess that we've got going on here.
So I'm going to delete all these friend functions right here.
And I'm going to lead their implementations as well. So I'm going to get rid of the account one,
and this will definitely clean up our code a lot. I'll get rid of this guy right here.
I'll get rid of that implementation for the checking account.
I'll get rid of the one in savings.
And I'll delete the implementation for the savings.
And then finally, we'll do the same for trust.
So I'll get rid of that, and I'll get rid of this as well.
Okay. So now we've got a much cleaner look -- let me scroll all the way up here.
So all we have now is the account class with its virtual void withdraw,
virtual destructor, checking with its virtual void withdraw,
virtual destructor and so forth. So we're at a really clean place to start.
So what do we do. Well, the first thing we want to do is we want to create
that printable class interface. So let's do that.
And of course, you would normally do this in .header files and .cpp files,
but I'm going to do it all in one just so it's easier and I don't have to switch between files to
be able to explain this.
So let's do that. So we'll create a class called,
let's just call it I printable. You can call it with or without that I, it's really up to you.
So that's my class.
And I'll close it off down here.
Now what do I want in this class. Well, I need a, friend, right. I have to have a friend because
the way that insertion operator works where the left side is the stream
and the right side is the object.
I really can't have member functions that do that kind of thing.
So what I want to do is I want to have a friend function that's very similar to the ones we had before.
So let's say friend, and it's going to return a std ostream
by reference.
We're going to overload the insertion operator.
And the two parameters are the std ostream and
the printable object, that's where things really change.
So let's create the std ostream
by reference
and a const I printable object
by reference.
We could just call that obj or our right-hand side, whatever you like.
So that's my function prototype.
And now what we want is we want to have a public
pure virtual function
that all classes that conform to this interface must implement.
That's the one that's going to allow them to put whatever they want on the stream, however they want.
So in this case, it'll be virtual,
let's say void, and we'll call it print, we can call it anything we like.
And we're going to put stuff on an ostream.
This should be const since we really don't want it modifying
any objects.
And here's the important piece that's a pure virtual function now.
Okay. So now let's implement that friend function.
I'll copy this so I don't have to type it all out again.
And what we want this function to do is really pretty straightforward.
All we want this function to do is do, let's say, obj, right.
That's the object, and we want to print onto that stream.
That's the virtual function that all of my concrete classes
must implement, and then I just simply return the stream.
That's it. Now notice what's happening here. This function gets called with a printable
or an I printable.
And it's going to call print on that object. So this will --
we're coming at it with a base class reference, right, if you will, because they're all going to be printable.
And we're going to use dynamic polymorphism to match the appropriate one.
So that's really all we need to do here. So that's a really, really simple
class interface.
We only need to do this with the account class because we've got a hierarchy here,
but I'll show you another way to do it as well in a second. So we could say public
I printable.
So now we're saying that that account class conforms to this I printable protocol,
which means that it must implement this guy right there, that virtual void function.
So we can do that right here. We can say virtual
void print,
right. It expects a std ostream by reference.
We can call it os,
and it is a const method.
And I want to override. I want to definitely put that in there so that the compiler can help me in case I messed up.
That's it. And what do we do here, we just simply say os
account, let's just say display.
That's it. That's all we have to do.
Now for all the other classes that are derived from this account class,
what we need to do is implement that virtual function in them
in whatever way makes sense to display a checking account.
In this case, I'll just say checking,
right. In this case, I'll just say savings. But again, whatever makes sense,
you want to display interest rates whatever, whatever makes sense for a display of
a savings account. And here we'll do the same thing
with the trust account.
Okay. That should do it. So now if we build and run,
you can see that we got what we wanted, right. There's the two pointers.
So p1 now points to an account. And I'm displaying accounts display.
And p2 is pointing to a checking, and I'm getting checking display.That's pretty cool.
Okay. And obviously these guys will still work too since they're -- it's doing static binding,
so that's, that's easy. But here's the difference.
Again, you might still be wondering, so far so what. Well,
the difference here is that suppose we want to create a class called dog,
and that dog class wants to be printable.
So it is derived from printable.
So all this dog class has to do is implement that interface.
So let's do that right here. I'll say public,
and the interface is right up here. I'll just copy this guy to save myself a little time because
it's the same function.
And how does the dog display itself. Well, maybe the dog says woof woof,
simple as that. So now what I can do here is I can create a dog object,
and I can do it dynamically, right. So I need to put a pointer in there.
That's it. And now I can simply put that dog object on the stream.
So I can cut and paste, I'll copy from here.
And rather than p1 this time, it's dog.
I'm de-referencing dog. And when I run now,
there you can see woof woof right there.
So that's pretty powerful.
Okay. Last thing is we can take it a step further. What we can do is we can actually write a function,
and I'll just copy it here so you can see what it would look like
right here.This is just a regular c++ function, and it's called print.
And what does it expect? It expects an i printable,
so it'll take anything. It'll take a dog object, an account object, a savings account object
because they're all printable.
So again, we could pass all of those guys into there. So if let's say I'm right here in my dog,
I could say print, and I can pass dog into it.
And if I run that, I should see woof woof twice. And there you can see it right up here.
You could do the same thing with the account objects.
This is where this is different
from just regular inheritance from a base class.
In this case, I can use this printable across hierarchies,
across anything I want. I can just say that whatever I'm creating is printable,
overload that print pure virtual function, and I'm good to go I'm a printable.
So remember, in this case dog is a printable,
an account was a printable, a savings account was a printable,
a savings account was also an account, this is a really good use case for it.