Mastering Virtual Functions

Mastering Virtual Functions is a milestone for every C++ developer. They are the engine behind Runtime Polymorphism, allowing your code to be extensible, maintainable, and "open for extension but closed for modification."
Let's explore what is the need for virtual functions, how they work under the hood, memory layout illustration and essential things to know before using them.
๐๐๐โ"๐๐๐ข":โ๐๐๐๐โ๐๐โ๐๐๐โ๐๐๐๐โ๐๐๐โ๐ ๐๐๐๐๐๐โ๐ต๐๐๐๐๐๐๐๐?
In C++, pointers are "static". If you have a Base class pointer pointing to a Derived object, calling a member function through that pointer will, by default, execute the Base version of that function. This happens because the compiler performs Early Binding (Static Binding), deciding which function to call based on the type of the pointer rather than the actual object.
In the above example when draw() function is called using base pointer(Shape*), the draw() function from base class is called rather than calling the draw() function from Circle, Line classes.
Problem: Imagine you are building a canvas that holds a list of different shapes: Circle, Line, and Rectangle. All of these inherit from a base class called Shape. if you store the objects of the derived classes in a list of Shape*, the compiler performs Early Binding and calls the Base version of that function for all the objects.
๐๐๐โ๐๐๐๐๐๐๐๐:โ๐ท๐๐ โ๐ ๐๐๐๐๐๐โ๐ต๐๐๐๐๐๐๐๐โ๐ต๐๐กโ๐๐๐๐
By marking a function as virtual in the base class, you tell the compiler: "Do not decide which function to call now. Wait until the program is running".
This enables Late Binding (Dynamic Binding). Even if you use a Base class pointer, C++ will look at the actual object type at runtime and calls the correct overridden implementation.
๐ท๐๐ โ๐ธ๐โ๐๐๐๐๐โ๐๐๐๐๐โ๐๐๐โ๐ท๐๐๐,โ๐ผ๐๐๐๐๐ขโ๐ธ๐๐๐๐๐๐๐๐๐๐๐
To resolve the actual functionโs address or definition at run time, C++ compiler adds some additional data structure around virtual functions i.e.
๐๐ง๐ฎ๐ฏ๐น๐ฒ: It is a static array of function pointers that contains the addresses all virtual functions of this class. Compiler builds this vTable at compile time. Every class that has at least one virtual member function in it has a vTable associated with it.
๐๐ฃ๐ผ๐ถ๐ป๐๐ฒ๐ฟ: A hidden pointer added to every object instance of a class with virtual functions. It points to the classโs vTable. This vPointer will be used to find the actual function address from vTable at run time.
Now assume, a new class called SlantedLine is derived from Line class and implemented the draw() function.
The memory layout after adding a new class will be as follows (new member variables are shown to represent that the vPointer is stored at the start of the object's memory):
what exactly happens when a call to virtual function is executed:
vPointer hidden in the first 4 bytes of the object will be fetched.
vTable of this class is accessed through the fetched vPointer.
From the vTable corresponding functionโs address will be fetched.
Function will be executed from that function pointer.
Note:
vTable stores the memory addresses of the most-derived versions of the virtual functions.
vPointer is initialized and whenever a constructor is called, and it is set to point to its corresponding class vTable.
๐ด๐๐๐๐๐๐๐๐โ๐๐๐๐๐๐โ๐๐โ๐๐๐๐๐๐๐๐
๐ฉ๐ถ๐ฟ๐๐๐ฎ๐น ๐๐ฒ๐๐๐ฟ๐๐ฐ๐๐ผ๐ฟ๐:
If a Derived object is deleted through a Base class pointer, only the Base class Destructor runs. This can lead to memory leaks if the derived class has allocated any memory or resources and never cleaned up.
In order to avoid such memory leaks, making the base destructor as virtual ensures the Derived destructor is called first, followed by the Base destructor, cleaning the entire object correctly.
๐๐ง๐ฎ๐ฏ๐น๐ฒ ๐ข๐ฟ๐ฑ๐ฒ๐ฟ ๐๐บ๐ฝ๐ผ๐ฟ๐๐ฎ๐ป๐ฐ๐ฒ:
In typical C++ development, the compiler manages vTable order internally by following the declaration order in the Base class. Any new virtual functions added only in the Derived class are appended to the end of the vTable, ensuring all indices inherited from the Base remain valid.
However, reordering the virtual functions in a published base class can change vTable indices and break binary compatibility for clients compiled against an older version. Hence, it is recommended to preserve the base class virtual functions order and append new virtual functions at the end.
๐๐ถ๐ฎ๐บ๐ผ๐ป๐ฑ ๐ฃ๐ฟ๐ผ๐ฏ๐น๐ฒ๐บ ๐๐ถ๐๐ต ๐บ๐๐น๐๐ถ๐ฝ๐น๐ฒ ๐ถ๐ป๐ต๐ฒ๐ฟ๐ถ๐๐ฎ๐ป๐ฐ๐ฒ:
Details on diamond problem are available here: Inheritance and Diamond Problem
๐๐ฑ๐ฑ๐ถ๐๐ถ๐ผ๐ป๐ฎ๐น ๐ก๐ผ๐๐ฒ๐:
Constructors cannot be virtual because an object's virtual mechanism is not fully set up until the constructor completes.
Virtual functions introduce a slight performance cost due to the extra indirection (dereferencing the vPointer and vTable) and also extra overhead of one vPointer for each object.
There is a concept of pure virtual function with no implementation in the base class, declared using the = 0 syntax (e.g., virtual void draw() = 0;). It forces derived classes to provide their own implementation. Any class containing at least one pure virtual function becomes an abstract class and cannot be instantiated
