Understanding C++ Polymorphism: How Virtual Functions Work Under the Hood
Polymorphism is a fundamental concept in C++ inheritance that enables runtime method resolution. While the concept itself is straightforward, many students struggle with complex polymorphism scenarios. Understanding the underlying mechanism—rather than simply memorizing the behavior—provides deeper insight and helps avoid common pitfalls.
Basic Polymorphism Example
Let’s start with a comprehensive example that demonstrates various polymorphism scenarios:
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
#include <iostream>
class Base
{
public:
virtual void print()
{
std::cout << "Print in Base" << std::endl;
}
};
class Derived_1 : public Base
{
public:
void print() override
{
std::cout << "Print in Derived_1" << std::endl;
}
};
class Derived_2 : public Base
{
public:
void print() override
{
std::cout << "Print in Derived_2" << std::endl;
}
};
class Derived_3 : public Base
{
// No override - inherits Base::print()
};
class Derived_1_1 : public Derived_1
{
// No override - inherits Derived_1::print()
};
int main()
{
Base base{};
base.print(); // OUTPUT: Print in Base
Derived_1 d1{};
d1.print(); // OUTPUT: Print in Derived_1
Derived_2 d2{};
d2.print(); // OUTPUT: Print in Derived_2
Derived_3 d3{};
d3.print(); // OUTPUT: Print in Base
// Since Derived_3 doesn't override print(), it calls Base::print()
// Polymorphism via pointers
Base *pd1 = &d1;
pd1->print(); // OUTPUT: Print in Derived_1
// Polymorphism in action - calls Derived_1::print()
// Polymorphism via references
Base &rd2 = d2;
rd2.print(); // OUTPUT: Print in Derived_2
// Reference enables polymorphism - calls Derived_2::print()
Base *pd3 = &d3;
pd3->print(); // OUTPUT: Print in Base
// Polymorphism works, but calls Base::print() (no override in Derived_3)
// Object slicing - no polymorphism
Base bd2 = d2; // Creates a new Base object (slicing occurs)
bd2.print(); // OUTPUT: Print in Base
// bd2 is a Base object, not a Derived_2 object
// Multi-level inheritance
Derived_1_1 d11{};
d11.print(); // OUTPUT: Print in Derived_1
// Inherits Derived_1::print()
Base &rd11 = d11;
rd11.print(); // OUTPUT: Print in Derived_1
// Polymorphism calls the most derived override
return 0;
}
The Mechanism Behind Polymorphism
Understanding how polymorphism works requires examining memory layout and function resolution at runtime.
1. Non-Virtual Functions
For regular (non-virtual) functions, the function call is resolved at compile time based on the static type:
1
2
3
4
5
6
7
class Foo
{
public:
void print();
};
Foo foo{};
The memory layout is straightforward - the function name directly points to the function’s memory location:
flowchart LR
subgraph MEMORY["Foo foo Object"]
direction TB
loc1["Member Data"]
end
subgraph FUNC["Foo::print()"]
direction TB
instr1["Instruction 1"]
instr2["Instruction 2"]
instr3["..."]
end
MEMORY -.-> instr1
2. Virtual Functions and the VTable
When a class declares virtual functions, the compiler automatically adds a hidden member variable called vptr
(virtual pointer) to enable runtime polymorphism.
Base Class with Virtual Function:
flowchart LR
subgraph MEMORY["Base base Object"]
vptr["vptr"]
data["Other Members"]
end
subgraph VTABLE["Base VTable"]
entry1["Base::print"]
end
subgraph FUNC["Base::print()"]
direction TB
instr1["Instruction 1"]
instr2["Instruction 2"]
end
vptr --> entry1
entry1 --> instr1
Derived Class with Override:
flowchart LR
subgraph MEMORY["Derived_1 d1 Object"]
vptr["vptr"]
data["Inherited + New Members"]
end
subgraph VTABLE["Derived_1 VTable"]
entry1["Derived_1::print"]
end
subgraph FUNC["Derived_1::print()"]
direction TB
instr1["Instruction 1"]
instr2["Instruction 2"]
end
vptr --> entry1
entry1 --> instr1
Derived Class without Override:
flowchart LR
subgraph MEMORY["Derived_3 d3 Object"]
vptr["vptr"]
data["Inherited + New Members"]
end
subgraph VTABLE["Derived_3 VTable"]
entry1["Base::print"]
end
subgraph FUNC["Base::print()"]
direction TB
instr1["Instruction 1"]
instr2["Instruction 2"]
end
vptr --> entry1
entry1 --> instr1
3. Polymorphic Function Resolution
When using pointers or references to base class objects, the runtime system follows the vptr
to resolve the correct function:
flowchart LR
subgraph PTR["Base *pd1"]
pointer["pd1"]
end
subgraph MEMORY["Derived_1 d1 Object"]
vptr["vptr"]
data["Object Data"]
end
subgraph VTABLE["Derived_1 VTable"]
entry1["Derived_1::print"]
end
subgraph FUNC["Derived_1::print()"]
direction TB
instr1["Function Body"]
instr2["Returns"]
end
pointer --> MEMORY
vptr --> entry1
entry1 --> instr1
4. Object Slicing: When Polymorphism Doesn’t Work
The assignment Base bd2 = d2;
invokes the copy constructor, creating a new Base
object. This is called object slicing because the derived portion is “sliced off”:
flowchart LR
subgraph ORIGINAL["Derived_2 d2"]
vptr1["vptr"]
basedata["Base data"]
deriveddata["Derived data"]
end
subgraph VTABLE1["Derived_2 VTable"]
entry1["Derived_2::print"]
end
subgraph SLICED["Base bd2 (new object)"]
vptr2["vptr"]
basedata2["Base data (copied)"]
end
subgraph VTABLE2["Base VTable"]
entry2["Base::print"]
end
vptr1 --> entry1
vptr2 --> entry2
ORIGINAL -.->|"copy constructor"| SLICED
Key Takeaways
Virtual Function Mechanism: Virtual functions are resolved through a vtable lookup at runtime, not compile time.
VPtr and VTable: Every object with virtual functions contains a hidden
vptr
that points to its class’s vtable.- Polymorphism Requirements: True polymorphism requires:
- Virtual functions in the base class
- Pointers or references to base class objects
- The actual object being of a derived type
Object Slicing: Direct assignment creates a new object of the static type, losing the derived behavior.
- Inheritance Chain: If a derived class doesn’t override a virtual function, it inherits the most recent override from its inheritance chain.
Understanding these mechanisms helps explain why polymorphism works in some cases but not others.