C++ Polymorphism

Polymorphism, as the name suggests — many forms. In plain terms, it means “one interface, multiple implementations.” (At the time of writing, my knowledge framework was not yet fully developed; please feel free to point out any omissions or errors.)


1. Why Do We Need Polymorphism?

In engineering, we frequently encounter ever-changing requirements. If we design a separate interface and logic for every requirement, it would inevitably lead to a large amount of code redundancy and reduced maintainability.

Consider the early days of mobile phone charging interfaces. At the beginning of the 21st century, every feature-phone manufacturer implemented proprietary charging interfaces. When you went out, no charger was as useful as a universal charger — it could charge your own device as well as someone else’s. In this case, the universal charger used a single interface to satisfy multiple charging needs, undoubtedly solving our charging problem efficiently.

Nowadays, although charging interfaces have largely been unified into the Type-C interface, the issue of incompatible fast-charging protocols still exists. While a single charger works everywhere when you’re out and about, the charging speed without fast charging is painfully slow. Naturally, devices and chargers supporting the PD fast-charging protocol become the preferred choice — they avoid the overhead of carrying an extra set of chargers while also solving the slow charging speed problem.

It is clear that a unified interface is extremely necessary. Programs should be the same way — this not only accommodates changing requirements but also avoids redundancy caused by duplicate code, achieving effectiveness through a constant approach to ever-changing situations.

2. Programming Implementation of Polymorphism

Let’s get straight to the code (source: “C++ Polymorphism | Runoob Tutorial”)

 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
#include <iostream> 
using namespace std;
 
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};
class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }
};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }
};
// Main function of the program
int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
 
   // Store the address of the rectangle
   shape = &rec;
   // Call the rectangle's area function
   shape->area();
 
   // Store the address of the triangle
   shape = &tri;
   // Call the triangle's area function
   shape->area();
   
   return 0;
}

When the above code is compiled and executed, it produces the following output:

1
2
Parent class area :
Parent class area :

Our original intention was to use the base class as a unified interface, deriving different subclasses for different requirements, pointing a base class pointer to an instantiated subclass object, and calling the subclass object’s method to produce the corresponding output. However, the actual output is the content of the base class method.

The program produces no errors, so why is the output different from what we expected? Because during compilation, the area method has already been set by the compiler to the version in the base class — this is called static linking, also known as early binding. At this point, the program determines the method to call based on the pointer’s type. This runs completely counter to our goal, so it naturally won’t work.

So how should we achieve this goal? We need to introduce the virtual keyword. Place the virtual keyword before the area method declaration in the base class Shape, as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

At this point, the compilation and execution results are as follows:

1
2
Rectangle class area :
Triangle class area :

As we can see, the program has achieved its goal: executing the methods of subclass objects and producing the corresponding output. Now we can use the base class as an interface, deriving subclasses for various different requirements to implement corresponding solutions.

At this point, with the virtual keyword, the compiler looks at the pointer’s content rather than its type. Therefore, since the addresses of tri and rec class objects are stored in *shape, their respective area() functions will be called.

As you can see, each subclass has its own independent implementation of the area() function. This is the general way polymorphism is used. With polymorphism, you can have multiple different classes, all with functions of the same name but with different implementations — the function parameters can even be the same.

A virtual function is a function declared with the virtual keyword in the base class. When redefining a virtual function defined in the base class in a derived class, it tells the compiler not to statically link to that function. What we want is to be able to select the function to call based on the object type at any point during program execution — this operation is called dynamic linking, or late binding.

3. From Virtual Functions to Polymorphism

3.1. “Overriding” Virtual Functions

Earlier, we used the virtual keyword to modify the area method of the base class Shape. At this point, this method can be called a virtual function. And “virtual functions” are precisely one of the prerequisites for implementing polymorphism.

It is necessary here to discuss function overriding, overloading, and redefining (hiding) to lay the groundwork for subsequent content:

  • Overloading: When function names are the same but parameter lists differ, and they are in the same scope, overloading is formed. The return value can be the same or different.
  • Overriding: This is like a substitution — in the scope of a derived class, the overridden virtual function must have the same function name, parameter list, and return value as the base class virtual function (with exceptions for covariant return types and destructors).
  • Redefining: This is name hiding in inheritance. When a function in a derived class has the same name as a function in the base class, regardless of whether the parameters are the same, as long as the function is not virtual, it does not constitute overriding but rather redefining (name hiding).

Implement virtual functions as interfaces in the base class, “override” those functions in derived classes, and then you can call the overridden virtual functions through base class pointers or references to achieve polymorphism.

When overriding a base class virtual function, even if the derived class’s virtual function does not use the virtual keyword, it can still constitute overriding (because after inheritance, the base class’s virtual function retains its virtual attribute in the derived class). However, this coding style is not standard and is not recommended.

3.2. Exceptions to Virtual Function Overriding

However, due to practical needs, there are exceptions to virtual function overriding:

  • Covariant Return Types
    When a derived class overrides a base class virtual function, and the only difference from the base class virtual function is the return type — where the base class virtual function returns a pointer or reference to the base class object and the derived class virtual function returns a pointer or reference to the derived class object — this is called covariance. The following example is quoted from “C++ Polymorphism”:

     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
    
    #include "iostream"
    using namespace std;
    
    class Person // Base class
    {
      public:
        virtual Person* pointer(void) // Returns Person*
        {
            cout << "Person* pointer(void)" << endl;
            return new Person;
        }
    };
    
    class Student : public Person // Derived class inherits from base class
    {
      public:
        virtual Student* pointer(void) // Returns Student*
        {
            cout << "Student* pointer(void)" << endl;
            return new Student;
        }
    };
    
    int main(void) {
        Person p, *ptr;
        Student s;
    
        ptr = &p;
        ptr->pointer(); // Calls base class method
    
        ptr = &s;
        ptr->pointer(); // Calls derived class method
        return 0;
    }
    // Output is as follows:
    // Person* pointer(void)
    // Student* pointer(void)
    

    As we can see, although the pointer() function returns different types, polymorphism is still achieved.

  • Destructor Overriding
    Because destructors have naming requirements, from a code perspective, the destructor names of derived and base classes are different, which would seem to break the requirements of polymorphism. However, the compiler performs special processing on all destructor names — after compilation, all destructor names are uniformly treated as destructor. Therefore, as long as the base class destructor is a virtual function, any derived class destructor will constitute overriding with the base class destructor simply by being defined.

    You can easily see the existence of virtual destructors in code with inheritance relationships. A base class virtual destructor ensures that when a derived class object pointed to by a base class pointer is destroyed, destruction proceeds layer by layer from inside to outside until the base class object is fully destroyed, avoiding the erroneous destruction that would occur if only the base class destructor were called and the derived class destructor were missed. For example:

     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
    
    #include "iostream"
    using namespace std;
    
    class Parent {
      public:
        Parent(void) { cout << "Parent has been created!" << endl; }
        ~Parent() { cout << "Parent has been deleted!" << endl; }
    };
    
    class Child : public Parent {
      public:
        Child(void) { cout << "Child has been created!" << endl; }
        ~Child() { cout << "Child has been deleted!" << endl; }
    };
    
    int main(void) {
        Parent* p = new Parent;
        cout << "-------------------------" << endl;
        Parent* c = new Child;
        cout << "-------------------------" << endl;
        delete p;
        cout << "-------------------------" << endl;
        delete c;
    
        return 0;
    }
    
    // Output is as follows:
    // Parent has been created!
    // -------------------------
    // Parent has been created!
    // Child has been created!
    // -------------------------
    // Parent has been deleted!
    // -------------------------
    // Parent has been deleted!
    

    When releasing the derived class object, only the base class object was released, causing a memory leak. The base class destructor should be decorated with the virtual keyword to make it a virtual destructor. At this point, even without adding virtual, the derived class destructor inherits the base class’s virtual function attribute and becomes a virtual destructor (in practice, it is recommended to explicitly add virtual to declare it as a virtual function). The output is as follows, showing complete destruction with no memory leak.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    // Output is as follows:
    // Parent has been created!
    // -------------------------
    // Parent has been created!
    // Child has been created!
    // -------------------------
    // Parent has been deleted!
    // -------------------------
    // Child has been deleted!
    // Parent has been deleted!
    

    But are virtual destructors always required? Not necessarily. Here we categorize situations where virtual destructors are not needed:

    • When there is no need to use a base class pointer to point to derived class objects — that is, when the base class is not used as an interface — a virtual destructor is not needed. Because derived class objects undergo destruction from inside to outside — first destroying the derived class, then the base class. At this point, destroying the derived class object allows full destruction from inside to outside without omissions. Example:

       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
      
      #include "iostream"
      using namespace std;
      
      class Parent {
        public:
          Parent(void) { cout << "Parent has been created!" << endl; }
          ~Parent() { cout << "Parent has been deleted!" << endl; }
      };
      
      class Child : public Parent {
        public:
          Child(void) { cout << "Child has been created!" << endl; }
          ~Child() { cout << "Child has been deleted!" << endl; }
      };
      
      int main(void) {
          Parent p;
          Child c;
      
          return 0;
      }
      
      // Output is as follows:
      // Parent has been created!
      // Parent has been created!
      // Child has been created!
      // Child has been deleted!
      // Parent has been deleted!
      // Parent has been deleted!
      

      Analysis: First, the base class object is instantiated, executing the base class constructor, then the derived class object is instantiated. Because during inheritance, constructors execute from outside to inside (opposite to destructors), the base class constructor executes first, followed by the derived class constructor. Then the derived class object and the base class object are destructed in order from inside to outside. As for the final base class destructor, it is determined by the stack-based execution of the program — last in, first out (data structure knowledge).

    • When all derived classes do not perform resource cleanup or other closing operations in their destructors, a virtual destructor is not needed. This is easy to understand: the purpose of destructors is to perform closing work when an object ends, such as processing data and releasing resources. If these operations are not involved, whether the destructor is correct or not does not affect program execution.

    • Classes that will not be publicly inherited do not need virtual destructors. Because private or protected inheritance prevents base class pointers from pointing to derived classes in non-friend functions and classes.

    In the above situations, it is acceptable not to declare destructors as virtual. Moreover, virtual functions can only determine the object type at runtime, requiring dynamic linking, which has greater overhead and lower efficiency compared to static linking. However, it is difficult to guarantee that the above situations are 100% followed, so in actual development, a comprehensive approach is needed — there is no universal rule that applies everywhere.

3.3. Notes on Virtual Functions

  • The compiler does not allow setting constructors as virtual functions. Constructors are automatically called when objects are created and cannot be called through base class pointers or references, so constructors cannot be virtual functions.

    Additionally, each virtual function corresponds to a virtual function table (vtable), and this vtable is stored in the object’s memory space. If a constructor were virtual, it would need to be called through the vtable, but the object has not yet been instantiated — meaning the memory space does not even exist, let alone being able to call through the vtable. Therefore, constructors cannot be virtual functions.

  • The virtual keyword only needs to be added at the virtual function’s declaration; it can be added or omitted at the function definition.

  • Only class member functions can be declared as virtual functions. Friend functions are not class member functions, so they cannot be declared as virtual functions.

  • Static member functions cannot be virtual functions. Static functions are bound at compile time, while virtual functions can only be determined at runtime. Furthermore, virtual functions have a hidden this pointer that belongs to the instantiated object and can only be called through an object; static member functions do not have a this pointer — they belong to the class, not to specific objects, and cannot be called through an object. Therefore, the two cannot coexist.

  • Inline functions cannot be virtual functions. Inlining expands function content at the function call site during compilation, trading space for time to improve runtime efficiency — it is static; while virtual functions are called dynamically, the caller is unknown at compile time, so they cannot be expanded inline — the compiler will ignore the inline request.

  • In the section on “Overriding” Virtual Functions, I mentioned redefining (name hiding). Here is a more detailed breakdown:

    • If a derived class function has the same name as a base class function and the parameters are different, then regardless of whether the virtual keyword is present, the base class function will be hidden (note: this is not equivalent to overloading).
    • If a derived class function has the same name as a base class function and the parameters are the same, but the base class function does not have the virtual keyword, the base class function is hidden (note: this is not equivalent to overriding because the return value is not checked); if the base class function has the virtual keyword, the compiler considers this an override of the base class virtual function, and if the return type differs from the base class function, it will report an error (with the exception of covariant return types).
    • If a derived class overrides a base class virtual function, then all other same-name functions in the derived class will be hidden, regardless of whether they are virtual functions.

3.4. Pure Virtual Functions

Adding “= 0” after a virtual function makes it a pure virtual function. A class containing pure virtual functions is called an abstract class (also called an interface class), and abstract classes cannot be instantiated. Derived classes also cannot be instantiated after inheritance — only by overriding the pure virtual functions can derived classes be instantiated. Pure virtual functions mandate that derived classes must override them, further emphasizing the characteristic of interface inheritance.

3.5. Extended Content on Virtual Functions

  • Here we introduce two additional keywords that may be encountered; awareness is sufficient:

    • final: When applied to a virtual function, it indicates that the virtual function cannot be inherited further. In plain terms, once the final keyword is added in the base class, it cannot be overridden in any derived class.
    • override: Checks whether a derived class virtual function actually overrides a base class virtual function. If it does not, the compiler reports an error. This is exactly the opposite of the final keyword — it is used in derived classes.
  • The significance of virtual functions for inheritance:

    • Implementation Inheritance
      Inheritance of regular functions is implementation inheritance — derived classes inherit the implementation of base class functions.
    • Interface Inheritance
      Virtual functions are interface inheritance — derived classes inherit the interface of base class virtual functions, with the purpose of overriding them to achieve polymorphism. In particular, the creation of abstract classes forces derived classes to override base class pure virtual functions; otherwise, derived classes cannot be instantiated, greatly diminishing the class’s functionality.
  • The virtual keyword can not only modify functions to make them virtual functions, but it can also be used to modify inheritance relationships to achieve virtual inheritance. Since I have not yet encountered a need for virtual inheritance, I will not elaborate on this topic here, leaving it for future discussion.

4. Underlying Principles of Polymorphism

The following section draws from “C++ — Understanding the Implementation of Polymorphism in One Article”:

The key to “polymorphism” lies in the fact that when calling a virtual function through a base class pointer or reference, the compiler cannot determine at compile time whether it is calling the base class or derived class function — this can only be determined at runtime. So what happens if we use sizeof to output the size of a class with virtual functions versus one without?

 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
class A 
{
public:
    int i;
    virtual void Print() { } // Virtual function
};

class B
{
public:
    int n;
    void Print() { } 
};

int main() 
{
    cout << sizeof(A) << ","<< sizeof(B);
    return 0;
}

// On a 64-bit system, the output is:
// 16,4
// 
// Here we explain the difference of 12:
// On a 64-bit operating system, pointer size is 8 bytes; due to memory alignment, 
// even if less than 8 bytes, 8 bytes of space is still allocated;
// So the class object size is two vptr sizes: 2*8=16, not 4+8=12, 
// hence the 8-byte difference mentioned later.

From the above results, we can see that the class with virtual functions has an extra 8 bytes. On a 64-bit machine, a pointer type is exactly 8 bytes — what is the purpose of these extra 8 bytes?

4.1. Virtual Function Table (Vtable)

Every class with “virtual functions” (or a derived class of such a class) has a virtual function table (vtable), and every object of that class contains a pointer to the virtual function table. The vtable lists the addresses of the class’s “virtual functions.” The extra 8 bytes are used to store the address of the virtual function table.

First, as is customary — here is the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Base class
class Base 
{
public:
    int i;
    virtual void Print() { } // Virtual function
};

// Derived class
class Derived : public Base
{
public:
    int n;
    virtual void Print() { } // Virtual function
};

In the above, the Derived class inherits from the Base class, and both classes have “virtual functions.” Their vtable structure can be understood as follows:

Virtual Function Table
Virtual Function Table

Polymorphic function call statements are compiled into a series of instructions that look up the virtual function address in the vtable using the address stored in the object pointed to by (or referenced by) the base class pointer, and then invoke the virtual function.

4.2. Proving the Role of the Virtual Function Table Pointer

Earlier, we used the sizeof operator to calculate the size of a class with virtual functions and found an extra 8 bytes (on 64-bit systems). These extra 8 bytes are the pointer to the virtual function table. The vtable lists the addresses of the class’s “virtual functions.”

The following code example proves the role of the “virtual function table pointer”:

 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
// Base class
class A 
{
public: 
    virtual void Func()  // Virtual function
    { 
        cout << "A::Func" << endl; 
    }
};

// Derived class
class B : public A 
{
public: 
    virtual void Func()  // Virtual function
    { 
        cout << "B::Func" << endl;
    }
};

int main() 
{
    A a;
    
    A * pa = new B();
    pa->Func(); // Polymorphism
    
    // On 64-bit systems, pointers are 8 bytes
    int * p1 = (int *) & a;
    int * p2 = (int *) pa;
    
    * p2 = * p1;
    pa->Func();
    
    return 0;
}

// Output is as follows:
// B::Func
// A::Func

Analysis:

  • On lines 25-26, the pa pointer points to a B class object, so pa->Func() calls the B class object’s virtual function Func(), outputting B::Func;
  • Lines 29-30 are designed to make p1 point to the first 8 bytes of the A class — the “virtual function table pointer” — and p2 point to the first 8 bytes of the B class — the “virtual function table pointer”;
  • Line 32 assigns the A class’s “virtual function table pointer” to the B class’s “virtual function table pointer”, effectively replacing the B class’s “virtual function table pointer” with the A class’s “virtual function table pointer”;
  • Due to the effect of line 32, line 33 calls the A class’s virtual function Func(), outputting A::Func.

Through the above code and explanation, we can effectively prove the role of the “virtual function table pointer.”

4.3. Summary of Polymorphism Implementation Principles

The “virtual function table pointer” points to the “virtual function table,” and the vtable stores the addresses of the class’s “virtual functions.”

When a virtual function is declared in a class, the compiler generates a virtual function table within the class (an array where each element is the entry address of a virtual function). The vtable is a data structure storing pointers to class member functions, automatically generated and maintained by the compiler. Member functions decorated with virtual are placed into the vtable by the compiler. If a derived class overrides a base class virtual function, the derived class virtual function’s entry address replaces the base class virtual function’s entry address in the table; otherwise, the base class virtual function’s entry address is used.

The index (subscript) of a base class virtual function in the vtable is fixed and does not change as the inheritance hierarchy increases. New virtual functions added by derived classes are placed at the end of the vtable.

When virtual functions exist, every object contains a pointer to the virtual function table (the vptr, which is always located at the beginning of the object). This pointer points to the vtable of the class to which the object belongs. During a runtime call, the vptr is initialized based on the object’s type, allowing the vptr to correctly point to the vtable of the class it belongs to. By looking up the table to index the correct virtual function for invocation, the characteristics of polymorphism are realized.

It can be said that the vtable pointer enables the realization of polymorphism, and the correct initialization of the vtable pointer determines whether the implementation works properly. In other words, before the vtable pointer is correctly initialized, we cannot call virtual functions.

5. Summary

A base class pointer can behave in the manner of the base class or in the manner of a derived class — it has multiple forms, or multiple ways of expressing itself. We call this phenomenon polymorphism.

Searching for the Chinese keyword “C++ Polymorphism” reveals the following conditions for forming polymorphism:

  • An inheritance relationship must exist;
  • The inheritance relationship must have virtual functions with the same name, and they must be in an overriding relationship;
  • A pointer or reference of the base class type must exist, through which virtual functions are referenced or called.

By default, through a base class pointer, you can only access the derived class’s member variables, but cannot access the derived class’s member functions. To resolve this awkward limitation and allow base class pointers to access derived class member functions, C++ introduced virtual functions. In C++, the sole purpose of virtual functions is to form polymorphism.

C++ provides polymorphism so that base class pointers can perform “comprehensive” access to the member variables and member functions of all derived classes (including direct and indirect derived classes), especially member functions. Without polymorphism, we can only access member variables.

In practical terms, for example, in a game where data needs to be updated after each round: without polymorphism, you would have to call each object’s update method individually; but with polymorphism, you can call the update method of derived class objects through base class pointers within a loop, reducing code volume while optimizing code logic.

Furthermore, polymorphism, based on dynamic linking, allows old code to call new code, improving code reusability and enabling backward-compatible extension. Therefore, it is commonly used in framework development.

Polymorphism has many other uses as well, which will not be enumerated here.

However, polymorphism is not a universal solution — its high flexibility comes at the cost of efficiency, and polymorphic calls are slower than regular member functions using static linking. Therefore, for the sake of efficiency, there is no need to declare all member functions as virtual functions.

This concludes the discussion on polymorphism. Thanks to the articles written by fellow developers that provided ideas for this piece. Let us all make progress together. Keep going!

References

Thanks to the following individuals for sharing their knowledge — wisdom shines through dissemination.