Virtual Functions

In the previous lesson on pointers and references to the base class of derived objects, we looked at several examples where using pointers or references to a base class had the potential to simplify code. However, in every case, we ran up against the problem that the base pointer or reference was only able to call the base version of a function, not a derived version.

Here's a simple example of this behavior:

#include <iostream>
#include <string_view>

class Entity
{
public:
    std::string_view getType() const { return "Entity"; }
};

class Enemy : public Entity
{
public:
    std::string_view getType() const { return "Enemy"; }
};

int main()
{
    Enemy goblin{};
    Entity& ref{ goblin };
    std::cout << ref.getType() << '\n'; // prints "Entity"

    return 0;
}

Because ref is an Entity reference, when ref.getType() is evaluated, it calls Entity::getType(), even though the underlying object is actually an Enemy.

In this lesson, we will show how to address this issue using virtual functions.

Virtual functions and polymorphism

A virtual function is a special type of member function that, when called, resolves to the most-derived version of the function for the actual type of the object being referenced or pointed to.

A derived class function is considered a match if it has the same signature and return type as the base class function. Such functions are called overrides.

To make a function virtual, simply place the virtual keyword before the function declaration.

Here's the above example with a virtual function:

#include <iostream>
#include <string_view>

class Entity
{
public:
    virtual std::string_view getType() const { return "Entity"; }
};

class Enemy : public Entity
{
public:
    virtual std::string_view getType() const { return "Enemy"; }
};

int main()
{
    Enemy goblin{};
    Entity& ref{ goblin };
    std::cout << ref.getType() << '\n'; // prints "Enemy"

    return 0;
}

Because ref.getType() is a virtual function, the compiler sees we're calling getType() on a reference to an Entity. It then determines that Entity::getType() is virtual. The call is deferred until runtime, at which point the program looks at the actual type of the object being referenced. Because ref is referencing an Enemy object, Enemy::getType() is called.

This prints:

Enemy

Let's take a look at a slightly more complex example:

#include <iostream>
#include <string>
#include <string_view>

class Entity
{
protected:
    std::string m_name{};

public:
    Entity(std::string_view name)
        : m_name{ name }
    {
    }

    const std::string& getName() const { return m_name; }
    virtual std::string_view getType() const { return "Entity"; }
};

class Enemy : public Entity
{
public:
    Enemy(std::string_view name)
        : Entity{ name }
    {
    }

    virtual std::string_view getType() const { return "Enemy"; }
};

class Boss : public Entity
{
public:
    Boss(std::string_view name)
        : Entity{ name }
    {
    }

    virtual std::string_view getType() const { return "Boss"; }
};

void describe(const Entity& entity)
{
    std::cout << entity.getName() << " is a " << entity.getType() << "\n";
}

int main()
{
    Enemy goblin{ "Goblin" };
    Boss dragon{ "Dragon" };
    Entity player{ "Player" };

    describe(goblin);
    describe(dragon);
    describe(player);

    return 0;
}

This program produces the following output:

Goblin is a Enemy Dragon is a Boss Player is a Entity

Three observations:

  1. Even though each object is being passed to describe() as an Entity reference, the correct derived getType() is called!
  2. We only needed to define describe() once, and it works for all three object types!
  3. This allows us to write functions that can work with any derived class without modification!

The ability to call derived functions through base class pointers or references is the core feature called polymorphism.

A more complex example

Let's take a look at another example that's a little more complex. First, here's a set of entity classes:

#include <iostream>
#include <string>
#include <string_view>

class Entity
{
protected:
    std::string m_name{};

public:
    Entity(std::string_view name)
        : m_name{ name }
    {
    }

    virtual std::string describe() const { return m_name; }
};

class Enemy : public Entity
{
public:
    Enemy(std::string_view name)
        : Entity{ name }
    {
    }
};

class Goblin : public Enemy
{
public:
    Goblin(std::string_view name)
        : Enemy{ name }
    {
    }

    std::string describe() const override
    {
        return m_name + " the Goblin";
    }
};

class Dragon : public Enemy
{
public:
    Dragon(std::string_view name)
        : Enemy{ name }
    {
    }

    std::string describe() const override
    {
        return m_name + " the Dragon";
    }
};

int main()
{
    Goblin gruk{ "Gruk" };
    Dragon smaug{ "Smaug" };

    Entity& ref1{ gruk };
    Entity& ref2{ smaug };
    std::cout << ref1.describe() << '\n';
    std::cout << ref2.describe() << '\n';

    return 0;
}

This outputs:

Gruk the Goblin Smaug the Dragon

In this example, ref1 is an Entity reference to gruk, which is a Goblin. When ref1.describe() is called, it resolves to Goblin::describe() because that's the most-derived matching function.

It's worth noting that although we defined Entity::describe() and Enemy doesn't override it, we can still override describe() in Goblin and Dragon. The function remains virtual all the way up the inheritance chain once it's made virtual in any base class.

Using virtual functions with pointers

Virtual functions work the same way when called with pointers:

#include <iostream>
#include <string_view>

class Weapon
{
public:
    virtual std::string_view attack() const { return "swinging weapon"; }
};

class Sword : public Weapon
{
public:
    virtual std::string_view attack() const { return "slashing with sword"; }
};

class Bow : public Weapon
{
public:
    virtual std::string_view attack() const { return "shooting arrow"; }
};

int main()
{
    Sword excalibur{};
    Bow longbow{};

    Weapon* weapon1{ &excalibur };
    Weapon* weapon2{ &longbow };

    std::cout << weapon1->attack() << '\n'; // prints "slashing with sword"
    std::cout << weapon2->attack() << '\n'; // prints "shooting arrow"

    return 0;
}

Do not call virtual functions from constructors or destructors

Here's another gotcha that often catches unsuspecting new programmers. You should not call virtual functions from constructors or destructors. Why?

Remember that when a derived class is created, the base portion is constructed first. If you were to call a virtual function from the base constructor, and the derived portion of the class hadn't been created yet, it would be unable to call the derived version of the function because there's no derived object for the derived function to work on. In C++, the derived portion hasn't been created yet when the base constructor executes, so calling the virtual function from the base constructor results in calling the base version.

A similar issue exists for destructors. If you call a virtual function in a base class destructor, it will always resolve to the base class version of the function, because the derived portion of the class has already been destroyed.

Best Practice
Never call virtual functions from constructors or destructors.

The downside of virtual functions

Since most of the time you'll want your functions to be virtual, why not just make all functions virtual? The answer is because it's inefficient -- resolving a virtual function call takes longer than resolving a regular one. Furthermore, the compiler also has to allocate an extra pointer for each class object that has one or more virtual functions.

Summary

Virtual functions: A virtual function is a member function that resolves to the most-derived version at runtime when called through a base pointer or reference. Use the virtual keyword before the function declaration to make it virtual.

Polymorphism: The ability to call derived functions through base class pointers or references is called polymorphism. This allows writing functions that work with any class in an inheritance hierarchy without modification, because the correct derived version is called automatically.

Virtual function resolution: When a virtual function is called through a base pointer or reference, the actual object's type determines which function version runs. This resolution happens at runtime, not compile time, enabling dynamic behavior based on the actual object type.

Inheritance of virtual: Once a function is declared virtual in a base class, it remains virtual in all derived classes, even if the virtual keyword isn't explicitly used. You can override it at any level of the inheritance hierarchy.

Constructor/destructor restrictions: Never call virtual functions from constructors or destructors. During construction, the derived portion doesn't exist yet, and during destruction, it has already been destroyed, so only the base version would be called.

Performance considerations: Virtual functions have overhead because resolution happens at runtime rather than compile time, and each object with virtual functions needs an extra pointer. Only use virtual when polymorphic behavior is needed.

Virtual functions are the mechanism that enables runtime polymorphism in C++, allowing you to write flexible, extensible code that can work with objects of different types through a common interface.