Skip to main content

Command Palette

Search for a command to run...

Mastering Virtual Functions

Updated
โ€ข5 min readโ€ขView as Markdown
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