A friend asked me to make a post about the mechanics of virtual functions in C++. I thought about it for a few days and decided to write a broader post about several topics dealing with classes and what happens under the hood when we use member functions, inheritance, and virtual functions.
Member Functions
You can think of member functions as no different than static members, or functions defined outside the class, except the compiler adds a hidden first parameter this
. That’s how a member function is able to operate on an instance of a class. So when you write this:
class C
{
public:
void MemFun(int arg) {}
};
C c;
c.MemFun(1);
What you are really getting is this:
class C {};
void C_MemFun(C* _this_, int arg) {}
C c;
C_MemFun(&c, 1);
A compiler generated C_MemFun
with extra parameter of type C*
.
Inheritance
Here, I want to briefly mention the order in which constructors and destructors are called: if D
derives from B
and you create an instance of D
, the constructors will be executed in order: B
s first, D
s second. So when the control enters D
s constructor, B
part of the class has been initialized already. The opposite is true for destructors. When you delete an instance of D
, the destructors will be executed in order: D
s first, B
s second. The code at the end of this post will demonstrate it. BTW, I’m skipping over virtual and pure virtual destructors in this post since that’s a topic that could easily become its own blog post.
Virtual Functions
Several things happen when we declare a virtual function: for each class, the compiler creates a hidden table called virtual function table. Pointers to virtual functions are stored in this table. Next, each instance of a class gets a hidden member variable called virtual function table pointer that, as the name implies, points at the virtual function table for that particular class. So an instance of B
has a hidden member pointing at B
‘s virtual function table, and an instance of D
… ditto. When you create virtual functions, the compiler generates the following dispatch code:
template<typename T, typename... A>
void VirtualDispatch(T* _this_, int VirtualFuncNum, A&&... args)
{
_this_->VirtualTablePtr[VirtualFuncNum](_this_, forward<A>(args)...);
}
For an instance of type T
called _this_
, it does a lookup in the VirtualTablePtr
for virtual function number VirtualFuncNum
, and calls it with _this_
as the first argument plus whatever extra parameters it accepts. Depending on the type of _this_
, VirtualTablePtr
will point at a different virtual function table and that’s more or less how we get runtime polymorphism in C++.
Complete Listing
#include <iostream>
#include <utility>
#include <cstdlib>
using namespace std;
#define VIRTUAL_FUNCTION_1 0
#define VIRTUAL_FUNCTION_2 1
typedef void(*VirtualFunctionPtr)(void*, int arg);
struct Base
{
static void BaseConstructor(void* _this_)
{
((Base*)_this_)->VirtualTablePtr = BaseVirtualTable;
((Base*)_this_)->Member1 = rand() % 100;
cout << "BaseConstructor(" << _this_ <<
", Member1 = " << ((Base*)_this_)->Member1 << ")" << endl;
}
static void BaseDestructor(void* _this_)
{
cout << "BaseDestructor(" << _this_ << ")" << endl;
}
static void BaseVirtualFunction_1(void* _this_, int arg)
{
cout << "Base(Member1 = " << ((Base*)_this_)->Member1 <<
")::BaseVirtualFunction_1(" << arg << ")" << endl;
}
static void BaseVirtualFunction_2(void* _this_, int arg)
{
cout << "Base(Member1 = " << ((Base*)_this_)->Member1 <<
")::BaseVirtualFunction_2(" << arg << ")" << endl;
}
VirtualFunctionPtr* VirtualTablePtr;
int Member1;
static VirtualFunctionPtr BaseVirtualTable[3];
};
VirtualFunctionPtr Base::BaseVirtualTable[3] =
{
&Base::BaseVirtualFunction_1,
&Base::BaseVirtualFunction_2
};
struct Derived
{
static void DerivedConstructor(void* _this_)
{
Base::BaseConstructor(_this_);
((Derived*)_this_)->VirtualTablePtr = DerivedVirtualTable;
((Derived*)_this_)->Member2 = rand() % 100;
cout << "DerivedConstructor(" << _this_ <<
", Member2 = " << ((Derived*)_this_)->Member2 << ")" << endl;
}
static void DerivedDestructor(void* _this_)
{
cout << "DerivedDestructor(" << _this_ << ")" << endl;
Base::BaseDestructor(_this_);
}
static void DerivedVirtualFunction_1(void* _this_, int arg)
{
cout << "Derived(Member1 = " << ((Base*)_this_)->Member1 <<
", Member2 = " << ((Derived*)_this_)->Member2 <<
")::DerivedVirtualFunction_1(" << arg << ")" << endl;
}
static void DerivedVirtualFunction_2(void* _this_, int arg)
{
cout << "Derived(Member1 = " << ((Base*)_this_)->Member1 <<
", Member2 = " << ((Derived*)_this_)->Member2 <<
")::DerivedVirtualFunction_2(" << arg << ")" << endl;
}
VirtualFunctionPtr* VirtualTablePtr;
int __Space_for_Base_Member1__;
int Member2;
static VirtualFunctionPtr DerivedVirtualTable[3];
};
VirtualFunctionPtr Derived::DerivedVirtualTable[3] =
{
&Derived::DerivedVirtualFunction_1,
&Derived::DerivedVirtualFunction_2
};
template<typename T, typename... A>
void VirtualDispatch(T* _this_, int VirtualFuncNum, A&&... args)
{
_this_->VirtualTablePtr[VirtualFuncNum](_this_, forward<A>(args)...);
}
int main(int argc, char** argv)
{
srand((unsigned int)time(NULL));
cout << "===> Base start <===" << endl;
Base* base = (Base*)operator new(sizeof(Base));
Base::BaseConstructor(base);
VirtualDispatch(base, VIRTUAL_FUNCTION_1, rand() % 100);
VirtualDispatch(base, VIRTUAL_FUNCTION_2, rand() % 100);
Base::BaseDestructor(base);
operator delete(base);
cout << "===> Base end <===" << endl << endl;
cout << "===> Derived start <===" << endl;
Base* derived = (Base*)operator new(sizeof(Derived));
Derived::DerivedConstructor(derived);
VirtualDispatch(derived, VIRTUAL_FUNCTION_1, rand() % 100);
VirtualDispatch(derived, VIRTUAL_FUNCTION_2, rand() % 100);
Derived::DerivedDestructor(derived);
operator delete(derived);
cout << "===> Derived end <===" << endl << endl;
return 1;
}
===> Base start <===
BaseConstructor(0x1005845d0, Member1 = 29)
Base(Member1 = 29)::BaseVirtualFunction_1(59)
Base(Member1 = 29)::BaseVirtualFunction_2(52)
BaseDestructor(0x1005845d0)
===> Base end <===
===> Derived start <===
BaseConstructor(0x10060b6a0, Member1 = 52)
DerivedConstructor(0x10060b6a0, Member2 = 80)
Derived(Member1 = 52, Member2 = 80)::DerivedVirtualFunction_1(40)
Derived(Member1 = 52, Member2 = 80)::DerivedVirtualFunction_2(79)
DerivedDestructor(0x10060b6a0)
BaseDestructor(0x10060b6a0)
===> Derived end <===
Program output.