DEV Community

Cover image for Demystifying Virtual and Abstract Functions
Jason C. McDonald
Jason C. McDonald

Posted on

Demystifying Virtual and Abstract Functions

EDIT: After some superb comments, I've made some corrections to my original code: (1) virtual destructors, and (2) use of the override and final keywords.


Have you ever noticed C++'s inheritance system behaving in a way you didn't expect? Perhaps the base class's function kept getting called, and you didn't know how to have the derived class's function called instead. Or maybe you encountered some weird code in the class definition, things like virtual and =0;.

These all relate to virtual inheritance, which isn't nearly as scary as it first looks! Let's create a basic example to demonstrate what's going on with virtual, and why it is so awesome.

Let's imagine that we have a basic class, Animal, and that we derive a new class Dog from it...

class Animal
{
public:
    Animal(){}

    void eat()
    {
        std::cout << "Nom nom nom" << std::endl;
    }

    void sit()
    {
        std::cout << "[stares blankly]" << std::endl;
    }

    void speak()
    {
        std::cout << "[undefined sound]" << std::endl;
    }

    ~Animal(){}
};

class Dog : public Animal
{
public:
    Dog(){}

    void sit()
    {
        std::cout << "[sits]" << std::endl;
    }

    void speak()
    {
        std::cout << "woof" << std::endl;
    }

    ~Dog(){}
};

class Cat : public Animal
{
public:
    Cat(){}

    virtual void sit()
    {
        std::cout << "[meows disdainfully and walks away]" << std::endl;
    }

    virtual void speak()
    {
        std::cout << "mew" << std::endl;
    }

    ~Cat(){}
};

int main()
{
    Dog* dog = new Dog();
    Cat* cat = new Cat();

    dog->eat();
    dog->sit();
    dog->speak();

    cat->eat();
    cat->sit();
    cat->speak();
}
Enter fullscreen mode Exit fullscreen mode

This class would define a basic animal, so we can reuse some functions, such as eat(), which are common to all animals.
Then we override sit() and talk() to be specific to Dog. It all works pretty well!

Nom nom nom
[sits]
woof
Nom nom nom
[meows disdainfully and walks away]
mew

Great! That does exactly what we want. However, one of the advantages of inheritance is that we can write functions like this...

void makeAct(Animal* critter)
{
    critter->eat();
    critter ->sit();
    critter->speak();
}
Enter fullscreen mode Exit fullscreen mode

So, instead of writing two (or more) functions that do the same thing for each type of animal, we just accept the base class (or a pointer/reference to it) as the argument type. Then, we can do this...

int main()
{
    Dog* dog = new Dog();
    Cat* cat = new Cat();

    makeAct(dog);
    makeAct(cat);
}
Enter fullscreen mode Exit fullscreen mode

Here is where things get weird! When we run this, we get...

Nom nom nom
[stares blankly]
[undefined sound]
Nom nom nom
[stares blankly]
[undefined sound]

Oy oy! That's not what we're looking for, is it? Where's our woofs and mews? Why isn't the dog sitting and the cat aloofing? They're all acting like the boring base Animal class!

This is where virtual functions come in handy. By default, the compiler looks to the base class for the function definition. virtual tells the computer to look at the derived class for the function definition instead.

Going hand-in-hand with this are abstract classes, a special type of virtual function that must be defined in the derived class. To break this down:

  • virtual functions may be overridden by the derived class; the derived's version of the function will be used, even if the base class is used as the data type.

  • abstract or pure virtual functions MUST be overridden by the derived class - they don't even have a definition in the base class.

eat() is pretty much the same among all animals, so that's fine as it is. However, we know that we should optionally override sit(); untrained animals would stare blankly, but some would respond in specific ways.

Looking at our class design, we also realize that speak() is a rather stupid function to define in Animal...printing "[undefined sound]" just looks dumb. Any animal we define should have a sound, or else explicitly say something like "[no sound]". So, we'll make this pure virtual.

DESIGN PRINCIPLE: Explicit is better than implicit. In other words, every situation should have some specifically designated action (or failure) in the code. This is also why we defined explicitly empty constructors and destructors in all our classes, instead of having the compiler implicitly define them.

So, let's rewrite so that sit() is virtual, and speak() is pure virtual.

Our base class should also have a virtual destructor.

class Animal
{
public:
    Animal(){}

    void eat()
    {
        std::cout << "Nom nom nom" << std::endl;
    }

    virtual void sit()
    {
        std::cout << "[stares blankly]" << std::endl;
    }

    virtual void speak() = 0;  // the `= 0;` literally means "not defined here"

    virtual ~Animal(){}
};
Enter fullscreen mode Exit fullscreen mode

Meanwhile, in the derived classes, we add the [override](http://en.cppreference.com/w/cpp/language/override) keyword (C++11 and later) to each of the functions we are overriding. This gives us compiler errors if we are trying to override a non-virtual function.

class Dog : public Animal
{
public:
    Dog(){}

    void sit() override
    {
        std::cout << "[sits]" << std::endl;
    }

    void speak() override
    {
        std::cout << "woof" << std::endl;
    }

    ~Dog() override {}
};
Enter fullscreen mode Exit fullscreen mode

We can also use the [final](http://en.cppreference.com/w/cpp/language/final) keyword (C++11 and later) instead of override if we don't plan to override that function later.

Dogs might make different sounds and behave differently, so we might derive from Dog and make classes for specific breeds. Cats, on the other hand, basically all say "mew", and none of 'em will sit for you, so there's no need to override those further! Thus, for the Cat class, instead of using override, let's just use final, to prevent further overriding!

class Cat : public Animal
{
public:
    Cat(){}

    void sit() final
    {
        std::cout << "[meows disdainfully and walks away]" << std::endl;
    }

    void speak() final
    {
        std::cout << "mew" << std::endl;
    }

    ~Cat() override {}
};
Enter fullscreen mode Exit fullscreen mode

Later, if we try to override the speak() or sit() function in a class derived from Cat, we'll get a compiler error.

Now let's rerun that code from earlier. Here it is again, in case you forgot. We haven't changed anything here!

void makeAct(Animal* critter)
{
    critter->eat();
    critter ->sit();
    critter->speak();
}


int main()
{
    Dog* dog = new Dog();
    Cat* cat = new Cat();

    makeAct(dog);
    makeAct(cat);
}
Enter fullscreen mode Exit fullscreen mode

When we run it, we see...

Nom nom nom
[sits]
woof
Nom nom nom
[meows disdainfully and walks away]
mew

Right on! Now everything works as we expect.

Now, one word of caution: if you define a function as pure virtual or abstract (i.e. virtual thefunc() = 0;, you MUST define it in each derived class. If you don't, you'll get a compiler error. Arguably, that's one of the benefits of pure virtual functions...the compiler is able to step in and keep you from doing stupid things.

That's it! virtual simply allows you to control where functions are called in a class inheritance. Not so scary now, is it?

Top comments (7)

Collapse
 
samipietikainen profile image
Sami Pietikäinen

Good introduction to virtual functions! Though there are couple of things I would add. First I would make the base class destructor virtual so the derived class instances are destructed correctly also through base class pointers. Also, in modern C++ (C++11 onwards) it is a good practise to use override keyword in the derived classes. This way the compiler can produce error if the function was not overridden (e.g. missing virtual in base class). It hurts a little inside to see objects not being deleted ;)

Collapse
 
codemouse92 profile image
Jason C. McDonald • Edited

Hm, TIL. I never encountered override until your comment. I'll start using that, and its cousin final. Thanks! :-)

In the meantime, I've edited to use that in the code above.

Collapse
 
drewmikola profile image
Drew Mikola

Good introduction to the topic, I only have one comment: the destructor of your base class should be marked virtual as well, since it is an inherited class. For the example it doesn't matter, however if both base and derived classes needed to perform unique and specific actions during destruction it will ensure that the object is fully destructed.

Collapse
 
codemouse92 profile image
Jason C. McDonald

Silly me - I should know that! (In my own code, I always do that.) I've edited accordingly.

Collapse
 
eljayadobe profile image
Eljay-Adobe • Edited

The one thing that I've seen developers have trouble grasping is that the advantage of inheritance and polymorphism is code reuse.

Not in the sense of code reuse by the derived class reusing the functions of the base class.

Rather in the sense of users of the base class not having to change their code when given an object of the derived class.

This substitutability is sometimes called the Liskov Substitution Principle.

It's hard to see the value of it in a small contrived example program. But it is an important capability in object-oriented programming for non-trivial programs.

One project I worked on, which been on-going for 10 years by the time I joined the team, had used inheritance for code reuse of the base classes by the dozens and dozens of derived classes. This kind of "code reuse abuse" meant that all the callsites had to check the object to see what type of object was being worked with, so the caller could do the THIS thing rather than the THAT thing with the object. That was a mess.

Collapse
 
codemouse92 profile image
Jason C. McDonald

Ergo one form of what I call "DRY spaghetti". :) Good insight.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.