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:
1 2 3 4 5 6 7 8 |
class C { public: void MemFun(int arg) {} }; C c; c.MemFun(1); |
What you are really getting is this:
1 2 3 4 5 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
#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 <===
Program output.
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 <===
Virtual tables are actually a lot more complex than what you’ve shown. In addition to the virtual functions they usually (unless the compiler is generating trunks) also contain the delta values for this to fix up the this. There is also typeinfo data, a pointer to the most derived (for dynamic cast support), vcall data for virtual base and virtual function in a virtual base support.
The best guide (and what everyone follows) is the Itanium ABI at https://itanium-cxx-abi.github.io/cxx-abi/abi.html.