Vtables
WARNING
Article is not complete, and is just up to get feedback right now. If there are areas of this article which you find hard to understand, please send me a message on discord on the parts which were unclear so i can improve the article
INFO
This article covers a topic that typically does not need to be thought about while programming, however in modding, it is important to understand how c++ compilers implement virtual functions under the hood.
Let's start off with some example code, where we have two classes, where class Cat
is inheriting from class Animal
class Animal {
public:
virtual ~Animal() = default;
virtual std::string getName() const {
return "<unknown animal>";
};
virtual int getPopulation() const {
return 0;
}
};
class Cat : public Animal {
public:
virtual ~Cat() = default;
virtual std::string getName() const override {
return "<cat>";
}
};
Notice how in our base class Animal, we declare a virutal function getName
which returns a generic string name, whereas in our class Cat, we override this function to return a more specific string.
Why do vtables exist?
Say that we want to call this getName
function for each element of a std::vector<std::shared_ptr<Animal>>
, how does the c++ compiler know what function it should be calling each time?
// Add instances of Animal and Cat to a vector
std::vector<std::shared_ptr<Animal>> animals = {};
animals.push_back(std::make_shared<Animal>());
animals.push_back(std::make_shared<Cat>());
// Call our getName function on each element in the vector
for (const auto& animal : animals) {
Log::Info("name: '{}'", animal->getName());
}
[INFO] name: '<unknown animal>'
[INFO] name: '<cat>'
How does the compiler know what function to call?
To implement this, the c++ compiler adds a new hidden pointer to the beginning of the class, which points to a classes virtual function table, or vftable/vtable for short. Inside of this vtable, it contains pointers to all virtual functions associated with the class:
struct Animal::vftable {
void* destructor; // Animal::~Animal();
void* getName; // Animal::getName();
void* getPopulation; // Animal::getPopulation();
};
struct Cat::vftable {
void* destructor; // Cat::~Cat();
void* getName; // Cat::getName();
void* getPopulation; // Animal::getPopulation();
};
The first thing to notice, is that the virtual functions for both of these classes are in completely identical order.
However, take a look at the
getName
and the destructor, and you will see that both contain pointers to that classes specific implementation of the function.Lastly, take a look at the
getPopulation
function, you will notice that in both vtables, it still points to the originalAnimal::getPopulation
implementation. That is because in our class Cat, we do not override this function and so it uses the one from the original class.
Hidden vftable pointers
As mentioned earlier, a hidden pointer is placed at the very beginning of the class to its vftable.
class Animal {
public:
Animal::vftable* vtbl; // <- Compiler generated pointer to the vftable
// placed exactly at the beginning of the class
// before any member variables
}
class Cat : public Animal {
public:
Cat::vftable* vtbl; // <- Notice how the pointer has been replaced with a
// pointer to the vftable which is owned by cat
}
Calling virtual functions
And so, going back to our example earlier, where we were iterating each animal in a vector, under the hood, it actually looks more like this:
for (const auto& animal : animals) {
Log::Info("name: '{}'", animal->vtbl->getName());
// ^ finds the function pointer inside of
// the vftable and calls that function,
// without having to know what the type of
// animal actually is.
}
Since when we inherit a class, we replace the vtable pointer, with a pointer to the new classes vtable, this handles switching what function gets called automatically, without the compiler having to know the types of each pointer in the vector.