More on Virtual Functions in C++

Posted in Programming cpp The Weeklies -

Last time, we have talked about Virtual Functions in C++, and get to know the keyword virtual, which must be set on the parent class’ functions that may be overidden by the children classes, or else the child’s versions of those functions may not be called at runtime (instead, the parent class’ versions would be called).

As there are still more stuff I find complicating about this topic, I want to write another part to clarify it out, first to myself, and hopefully it can help some others as well. If you haven’t seen the previous post, check it out first, since this post will be a continuation of that one.

Why it was designed that way?

If you’re like me and having a lot of experience in a more modern language (for me it’s Python), you may have gotten the same question that I did: “Why the virtual keyword needs to be put, or else function overide won’t work? Shouldn’t it be the other way around: polymorphism should be the default, and we should only have to say something in case we don’t want it?”

From my perspectives, there are two main reasons for this design choice:

  • First, remember that C++ is a language that prioritizes speed and explicity: The language should not do too much by defaults, and programmers are supposed to be explicit about everything they want. For the compilers, to always derive to the children is slower than only do so if asked for and run the base class’ version otherwise.
  • And second, there’s actually a big gap between asking the compiler to always derive to the children’ functions, and enforcing that the function should never be overwritten. For example, it’s possible to have a situation where the parent class overwrites some function from a grandparent class, but wants to enforce that its children cannot overwrite the same function anymore. For that reason, it actually makes more sense to require that the programmer be explicit in whether or not they want to allow polymorphism, instead of setting it by default.

Of course, having no default behavior can make the compiler do unexpected things (which is called undefined behaviors). Recall that in the last post, we experienced a case when the child’s function was still called (Dog.speak() instead of Animal.speak()), even though there was no virtual keyword.

Implied virtual functions

Consider the following situation, in which a new class Puppy inherits class Dog.

class Animal {...};

class Dog: public Animal {...};

class Puppy: public Dog
{
public:
    Puppy(std::string name)
        : Dog { name }{}
    std::string speak() { return "Aww"; }
};

void echo(Animal *animal){..};

int main()
{
    Puppy puppy { "Luffy" };
    echo(&puppy);
    return 0;
}

This results in

Animal is named Luffy, and it says Aww

You can see that even though we didn’t use virtual keyword in Dog.speak(), Puppy.speak() still overwrites Dog.speak(). That is because the virtual property was implied for Dog.speak(), as it inherits Animal.speak().

Suppose you find that it doesn’t make senses for puppies to say “Aww”, as any kind of dogs should say “Woof”, you can prevent Puppy or any child class of Dog from overriding speak(). The keyword final can be added to a class function should you want to forbid overriding of that function, as follow:

class Dog: public Animal
{
public:
    Dog(std::string name)
        : Animal{ name }{}
    std::string speak() final { return "Woof"; }
};
class Puppy: public Dog {...}

This will return in a compiler error, which complains that Puppy.speak() overrides the final function Dog.speak(). The term “compiler error” might have made you nervous, but it’s what we want here: we should be noticed if there’s a mistake (a final function was overridden), rather than letting the code compiled and cause errors at run time (Runtime error), or worse, unexpected behaviors.

Pure virtual functions

At this point you may have questioned: “What if we only want a function that MUST be overridden? A clear example is what we are having here: It makes no sense to have an Animal saying ”???" by default (as no animal would do so). Instead, any class that inherits Animal should override function speak(), so that the speak() function in Animal should never be called. We should also get notified (a compiler error would be perfect) should we forget to override any of those requried functions in the child class.

That requirement can be achieved with something called pure virtual function. Basically, it’s a way of telling the compiler that this function is required, but it’s currently empty and will be implemented by the children classes, so do not complain here. The syntax is quite simple as follow:

class Animal
{
...
public:
    ...
    virtual std::string speak() = 0;
};

By “assigning” a virtual function to 0, we have made it pure virtual. Naturally, as this function cannot be called directly, class Animal cannot be used to declare a variable, like follows:

Animal animal { "Mickey" };

which throws a compiler error:

error: cannot declare variable ‘animal’ to be of abstract type ‘Animal’

However, it’s still totally fine to use Animal class as an abstraction of any of its children (or grand children, or great grand children, or great great …, you get the idea), like what we’ve already had in echo(Animal *animal). Hence, Animal is called an abstract class. Abstract classes (and pure virtual functions) are a great way to achieve polymorphism without the need to implement unrealistic and unnecessary default functions (which should never be called), and getting error if a required function is missing in a child class or somewhere else down the family tree. If you’re curious what error we would get if we forget to override a pure function, simply create a new class which inherits Animal (Cat or Duck are both good choices), and “forget” to have a speak() function.

Interface classes

In the last example, we notice that of two functions in Animal, only speak() was a pure virtual function, whilst getName() was not. We call Animal an abstractioon class. If a class has all of its functions declared as pure virtual, we call it an Interface class. It’s a more common pattern than an Abstraction class, as it’s easier to remember to override all functions than some of them. A common way to name those interface classes is to have an I prefix (like IAnimal), to denote that it is an interface. Also, recall that when a class is inherited, any function without virtual keyword can be taken as-is in children classes, so it’s quite common that interface classes have a virtual destructor (e.g. virtual ~IAnimal()), so that its children can have their own destructors explicitly defined (hence avoid possile memory leaking caused by default Destructor not aware of memory allocation in children classes).

Other than these small notations, there’s not many special things (that I know of) we need to remember when working with Interface classes. However, I just want to (re-)emphasize that it’s a very useful and commonly seen structure in C++, so make sure to get yourself comfortable around it.

That’s it for today. See you in the next post!

Written by Huy Mai