When you write this (I've replaced all usercode with lowercase):
class cat {
public:
virtual void speak() {std::cout << "meow\n";}
virtual void eat() {std::cout << "eat\n";}
virtual void destructor() {std::cout << "destructor\n";}
};
The compiler generates all of this magically (All my sample compiler code is uppercase):
class cat;
struct CAT_VTABLE_TYPE { //here's the cat's vtable type
void(*speak)(cat* this); //contains a pointer for each virtual function
void(*eat)(cat* this);
void(*destructor)(cat* this);
};
extern CAT_VTABLE_TYPE CAT_VTABLE; //later is a global shared copy of the vtable
class cat { //here's the class you typed
private:
CAT_VTABLE_TYPE* vptr; //but the compiler adds this magic member
public:
cat() :vptr(&CAT_VTABLE) {} //the compiler initializes the vtable ptr
~cat() {vptr->destructor(this);} //redirects to the one you coded
void speak() {vptr->speak(this);} //redirects to the one you coded
void eat() {vptr->eat(this);} //redirects to the one you coded
};
//Here's the functions you programmed
void DEFAULT_CAT_SPEAK(CAT* this) {std::cout << "meow\n";}
void DEFAULT_CAT_EAT(CAT* this) {std::cout << "eat\n";}
void DEFAULT_CAT_DESTRUCTOR(CAT* this) {std::cout << "destructor\n";}
//and the global cat vtable (shared by all cat objects)
const CAT_VTABLE_TYPE CAT_VTABLE = {
DEFAULT_CAT_SPEAK,
DEFAULT_CAT_EAT,
DEFAULT_CAT_DESTRUCTOR};
Well, that's a lot isn't it? (I actually cheated slightly, since I take the address of an object before it's defined, but this way is less code and less confusing, even if technically uncompilable) You can see why they built it into the language. And... here's SmallCat before:
class smallcat : public cat {
public:
virtual void speak() {std::cout << "meow2\n";}
virtual void destructor() {std::cout << "destructor2\n";}
};
and after:
class smallcat;
//here's the smallcat's vtable type
struct SMALLCAT_VTABLE_TYPE : public CAT_VTABLE_TYPE {
//contains no additional virtual functions that cat didn't have
};
extern SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE; //later is a global shared copy of the vtable
class smallcat : public cat { //here's the class you typed
public:
smallcat() :vptr(&SMALLCAT_VTABLE) {} //the compiler initializes the vtable ptr
//The other functions already are virtual, nothing additional needed
};
//Here's the functions you programmed
void DEFAULT_SMALLCAT_SPEAK(CAT* this) {std::cout << "meow2\n";}
void DEFAULT_SMALLCAT_DESTRUCTOR(CAT* this) {std::cout << "destructor2\n";}
//and the global cat vtable (shared by all cat objects)
const SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE = {
DEFAULT_SMALLCAT_SPEAK,
DEFAULT_CAT_EAT, //note: eat wasn't overridden
DEFAULT_SMALLCAT_DESTRUCTOR};
So, if that's too much to read, the compiler makes a VTABLE object for each type, which points to the member functions for that particular type, and then it sticks a pointer to that VTABLE inside each instance.
When you create a smallcat
object, the compiler constructs the cat
parent object, which assigns the vptr
to point at the CAT_VTABLE
global. Immediately after, the compiler constructs the smallcat
derived object, which overwrites the vptr
member to make it point at the SMALLCAT_VTABLE
global.
When you call c->speak();
, the compiler produces calls it's copy of cat::speak
, (which looks like this->vptr->speak(this);
). The vptr
member might be pointing at the global CAT_VTABLE
or the global SMALLCAT_VTABLE
, and that table's speak
pointer is therefore pointing either at DEFAULT_CAT_SPEAK
(what you put in cat::speak
), or DEFAULT_SMALLCAT_SPEAK
(the code you placed in smallcat::speak
). So this->vptr->speak(this);
ends up calling the function for the most derived type, no matter what the most derived type is.
All in all, it is admittedly very confusing, since the compiler is magically renaming functions at compile time. Actually, due to multiple inheritance, in reality it's far more confusing than I've shown here.