C++ Virtual Functions
Runtime vs. compile time polymorphism
Like all object-oriented languages, C++ has features designed to facilitate polymorphism. In C++ there’s the added capability to specify whether polymorphism is defined at runtime or at compile time. Although this may seem like semantics, the differences can lead to completely different outcomes and functionality.
Quick Definition: Polymorphism
The word polymorphism means to appear in many form. If you look it up in relation to object-oriented programming, it seems that everyone insists on using their own definition. Ultimately, even if the wording is different, all the definitions revolve around the ability to subclass objects and to pass along traits and behaviors through inheritance. This sort of definition can be too abstract to be of much use, so in this article, I’m going to dive into polymorphism and runtime vs. compile time polymorphism in C++.
Inheritance
Before we go any further, let’s touch on inheritance. Inheritance is when a subclass gains behaviors and data of the superclass or parent class from which it is inheriting.
Phone
Think of a phone for just one minute. A phone has a fundamental property, which is the phone number. It also has some fundemental behaviors, which is that it can make and receive calls.
Cell Phone
Now let’s think of a derivative, or subclass of a phone. That’s relatively easy, a cell phone. A cell phone still has the fundamental characteristics of a phone, that is it still has a phone number, and it can still make and receive calls. A cell phone is a phone but with some added behaviors like roaming and texting.
Computer
Now let’s get a bit more tricky. Let’s think of the characteristic of a computer to browse the web. This computer has a property that is fundemental which is the IP Address (Internet Protocol). It also has a piece of software called a web browser. Finally there is the intrinsic behavior you get when you combine them called “browsing the web”.
But can we think of anything that inherits characteristics of both a phone and a computer?
iPhone
The iPhone! The iPhone doesn’t fit exclusively into either category. Instead it it has the characteristics of both. Therefore an iPhone inherits both from a cell phone and from a computer (and cell phone inherits from phone.)
Can I Borrow Your Phone For Sec?
What if we had a function called borrow phone, whose only parameter is to accept a phone object. This function uses the passed in phones call function. Could we give it a cell phone? We sure could! We could even give it an iPhone or even an old-timey rotary phone. All this function wants is some object that is a phone. Now, what if we had a function took in a computer and used that computer to surf the web? Could this function take in a phone? Nope! Could it take in a cell phone? Nope! Could it take in an iPhone? It sure could! Now here’s the tricky part, could it use that iPhone to make a call? The answer to this is a bit more complicated, let’s just leave the answer for now as no.
Animals In The Zoo
To begin with, let’s start with a simple example of polymorphism. We’re going to build a wolf! To get started, let’s create our base class, which will be the class from which everything else inherits from, we’ll call it Animal.
Now we’ll create a subclass of animal, called Canine (since wolves are canines). When we do that, let’s also call our superclass Animal in our initializer, we can do that with :
Canine():Animal()
Now let’s create another subclass of animal called Carnivore, and again, we’ll call our super classes default method before adding our functionality.
Finally let’s make our Wolf!
You might have noticed, that while Wolf calls Carnivore and Canine, it doesn’t call Animal. That’s because Carnivore and Canine already do that for us.
Running Our Wolf
In your main function instantiate a new Wolf object like so:
When you run this, you should get something that looks like this.
Console output:
AnimalCanineCarnivore WolfProgram ended with exit code: 0
Wait Just A Minute…
Now you might be wondering why this small line:
Wolf* pet_wolf = new Wolf();
gave us four lines of output. Well that’s the inheritance in action! What we’re doing is calling the default implementation of the superclass all the way down the inheritance tree from Animal to Wolf.
This is the line where all the super class are being called:
Wolf():Carnivore(), Canine() { cout<< “Wolf” << endl; }
Now, a good question to raise at this point would be: Is it always necessary to call the superclass? The short answer is no, and the longer answer is that you should do it most of the time unless you have a specific reason not to.
Overriding Default Functionality
One feature of subclasses is that they can override the default functionality from the superclass. This can be useful for tailoring our subclasses for specific situations. To see how that works, let’s do another example. We’re going to make our Wolf again, but this time he lives in a Zoo!
The Animal class is mostly the same, but we’ve added a string property called name, which gets set from a parameter that is passed in during initialization. It also has a method called make_sound, which takes a string and outputs it to the console.
Our Canine and Carnivore classes are still unmodified.
Now let’s create our Wolf! This time our Wolf has two methods, howl and make_sound, which it inherits from the superclass.
And finally we need a zoo to house them!
In main, let’s instantiate our wolf, and give it a name. Then let’s put our wolf into our zoo.
If we run this we should see something like this.
Console output:
wolfWolfyNEWArooooProgram ended with exit code: 0
Compile time Polymorphism Vs. Runtime Polymorphism
Interesting right? We’re overriding the functionality for our wolf for make_sound, but not when our wolf is placed in the zoo. So how do we make it so we can hear the wolf howl in the zoo? We can make our superclass’ make_sound function into a virtual function and override it in our subclass!
Let’s add the virtual modifier to our make_sound function:
And we’ll add override below:
When you run your code you should see something like below.
Console output:
wolfWolfyArooooArooooProgram ended with exit code: 0
So what happened?
Inlining and Virtualization
In as simple an explanation as possible, we went from static dispatch to dynamic dispatch. In static dispatch, the compiler uses inlining which replaces a method/function call with the actual implementation. Essentially, it hardcodes. This can remove the computational overhead of the more dynamic method calls, but it comes at the expense of being less dynamic. It’s also a tradeoff between optimizing for space and optimizing for performance.
With static dispatch, the compiler does not virtualize. Without getting too sidetracked, virtualization is when your program figures out which implementation of the code it is going to use at runtime, instead of compile time. If that means absolutely nothing to you, don’t worry. I’m planning to write up some more on dispatching soon, so if you’re confused I’m going to come back to this. But for the purposes of this post, virtual keyword signals to the compiler that you want it to take into account runtime polymorphism instead of compile time polymorphism, which in practical terms, means you can change the underlying functionality depending on what get’s passed in at runtime.