An overview,

To achieve C++ polymorphism, C++ uses a technique of dynamic binding. The core of this technique is virtual tables (hereafter referred to as virtual tables). This article describes how virtual function tables are dynamically bound.

Class virtual table

Every class that contains virtual functions contains a virtual table.

We know that when A class (A) inherits from another class (B), class A inherits the right to call A function of class B. So if a base class contains virtual functions, then its descendant classes can call those virtual functions. In other words, if a class inherits from a base class that contains virtual functions, that class also has its own virtual table.

Let’s look at the following code. Class A contains virtual functions vfunc1, vfunc2. Since class A contains virtual functions, class A has A virtual table.

class

A

public
private
int

The virtual table of class A is shown in Figure 1.

Figure 1: Virtual representation intents for class A

A virtual table is an array of Pointers whose elements are Pointers to virtual functions, each of which corresponds to a function pointer to a virtual function. It is important to point out that ordinary functions are non-virtual functions that do not need to be called through the virtual table, so the elements of the virtual table do not contain Pointers to ordinary functions.


The assignment of an entry in a virtual table, that is, a pointer to a virtual function, occurs at compile time in the compiler, meaning that the virtual table can be constructed at compile time in the code.

Virtual table pointer

Virtual tables belong to classes, not objects. A class needs only one virtual table. All objects of the same class use the same virtual table.

To specify the virtual table of the object, the object contains a pointer to the virtual table that it uses. To make every object containing a virtual table have a virtual table pointer, the compiler adds a pointer to the class, *__vptr, to point to the virtual table. This way, when the object of the class is created, it has the pointer, and the value of the pointer is automatically set to point to the virtual table of the class.

Figure 2: Object and its virtual table

If the base class of an inherited class contains virtual functions, the inherited class also has its own virtual table, so the object of the inherited class also contains a pointer to the virtual table.

Dynamic binding

At this point, you must be curious about how C++ uses virtual tables and virtual table Pointers for dynamic binding. Let’s start with the following code.

class

A

public
private
int
class

B
public
A

public
private
int
class

C
public
B

public
private
int

Class A is the base class, class B inherits from class A, and class C inherits from class B. The object models of class A, class B and class C are shown in Figure 3 below.

Figure 3: Object models of classes A, B, and C

Because all three classes have virtual functions, the compiler creates A virtual table for each class: virtual table of class A (A VTBL), virtual table of class B (B VTBL), and virtual table of class C (C VTBL). Objects of class A, B, and C all have A virtual table pointer, *__vptr, that points to the virtual table of their own class.

Class A contains two virtual functions, so A VTBL contains two Pointers to A::vfunc1() and A::vfunc2().

Class B inherits from class A, so class B can call functions of class A, but since class B overrides the function B::vfunc1(), two Pointers to B VTBL point to B::vfunc1() and A::vfunc2().

C inherits from B, so C can call functions of B, but because C overrides C::vfunc2(), two Pointers to C VTBL point to B::vfunc1() (a function that points to the nearest inherited class) and C::vfunc2().

Although Figure 3 looks a bit complicated, it is possible to quickly picture the object models of these classes in your mind by capturing the fact that the virtual table pointer of an object points to the virtual table of the class it belongs to, and the pointer in the virtual table points to the virtual function of the closest class it inherits.

Non-virtual functions are not called through the virtual table, so there is no need for Pointers in the virtual table to point to these functions.

Suppose we define an object of class B, bObject. Since bObject is an object of class B, bObject contains a pointer to the virtual table of class B.

Now, we declare A pointer P of class A to the object bObject. Although p is a pointer to the base class that can only point to part of the base class, the virtual table pointer is also part of the base class, so P can access the virtual table pointer to bObject. BObject’s virtual table pointer points to the virtual table of class B, so P can access B VTBL. See Figure 3.

int
B
A
What happens when we call the vfunc1() function with p?

When the program executes p->vfunc1() and finds that p is a pointer and the function called is virtual, the following steps follow.

First, the virtual table corresponding to the object bObject is accessed according to the virtual table pointer P ->__vptr. Although the pointer P is of base class A* type, *__vptr is also part of the base class, so the object’s corresponding virtual table can be accessed by p->__vptr.

Then, the entry for the function being called is looked up in the virtual table. Since the virtual table can be constructed at compile time, it is possible to locate the corresponding entry in the virtual table based on the function being called. For a call to p->vfunc1(), the first entry of BVTBL is the corresponding entry of vfunc1.

Finally, the function is called based on the function pointer found in the virtual table. As you can see from Figure 3, the first entry of B VTBL points to B::vfunc1(), so p->vfunc1() essentially calls the B::vfunc1() function.

What if P points to an object of class A?

When aObject is created, its virtual table pointer __vptr is set to point to A VTBL so that p->__vptr points to A VTBL. Vfunc1 in A VTBL corresponds to the A::vfunc1() function in the entry, so p->vfunc1() essentially calls A::vfunc1().

The above three steps of calling a function can be expressed as follows:

As you can see, by using these virtual function tables, it is possible to call the virtual function of the running object correctly, even if the function is called using a pointer to the base class.

We call the process of calling a virtual function through a virtual table dynamic binding, and the phenomenon it manifests is called runtime polymorphism. Dynamic binding differs from traditional function calls, which we call static binding, where the call is determined at compile time.

So, when is dynamic binding of functions performed? This requires meeting the following three conditions.

  • Call a function through a pointer
  • Upcast (The conversion from an inherited class to a base class is called an upcast, and you can see resources in this article for what an upcast is)
  • A virtual function is called
If a function call meets the above three criteria, the compiler compiles the function call into a dynamic binding, using the same virtual table mechanism described above.

Five, the summary

Encapsulation, inheritance and polymorphism are three characteristics of object-oriented design, and polymorphism is the key of object-oriented design. C++ realizes the dynamic binding between virtual functions and objects through virtual function table, thus building the foundation of C++ object-oriented programming.

The resources

  • C++ Primer 3rd edition, Chinese version, translated by pan aimin et al
  • www.learncpp.com/cpp-tutoria…
  • Hou jie “C++ best programming practices” video, geek class, 2015
  • Upcasting and Downcasting,
    www.bogotobogo.com/cplusplus/u…


The appendix

The sample code