- Description: A note on the C++ virtual functions, virtual destructors, and virtual inheritance
- My Notion Note ID: K2A-B1-8
- Created: 2018-09-14
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
1. Virtual Functions
- Virtual functions = runtime polymorphism. Call through base pointer/ref → derived impl invoked based on dynamic type.
1.1 What Can and Cannot Be Virtual
| Function type |
Can be virtual? |
Why |
| Regular member function |
Yes |
This is the standard use case |
| Destructor |
Yes (and often should be) |
See section 2 |
| Constructor |
No |
The object's type is not yet determined during construction |
static member function |
No |
Static functions belong to the class, not an instance; no vtable lookup |
inline function |
Technically yes, but… |
The inline hint is ignored when called polymorphically through a pointer/reference |
1.2 Behavior During Construction
- Virtual call inside ctor resolves to current class's version, not derived's.
- During
Base construction, derived portion not yet initialized → vtable still points to Base.
#include <iostream>
class Base {
public:
Base() {
foo();
}
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() {
foo();
}
void foo() override {
std::cout << "Derived::foo()" << std::endl;
}
};
int main() {
Derived d;
return 0;
}
- Same applies to virtual calls from derived initialization list → invoke base impl (derived not yet initialized).
2. Virtual Destructors
- Class used as base (esp. with polymorphic delete via base pointer) → destructor must be virtual.
- Without virtual dtor,
delete of derived via base pointer = UB; typically leaks derived's resources.
#include <iostream>
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived;
delete ptr;
return 0;
}
- Rule of thumb: any virtual functions → virtual destructor. When in doubt →
virtual ~ClassName() = default;.
3. Virtual Inheritance (The Diamond Problem)
- "Diamond problem" — multiple inheritance where 2 parents share a common base. Graph forms a diamond:
Base
/ \
Derived1 Derived2
\ /
Diamond
Diamond gets two Base copies — one via Derived1, one via Derived2. Problems:
- Ambiguity — referring to
Base member through Diamond (e.g. d.x) ambiguous; compiler can't tell which copy.
- Wasted memory —
Base duplicated even when design wants one shared copy.
- Virtual inheritance tells compiler "all paths through this base share a single subobject." Resolves both.
3.1 The Problem (Without Virtual Inheritance)
class Base {
public:
int x;
};
class Derived1 : public Base {};
class Derived2 : public Base {};
class Diamond : public Derived1, public Derived2 {};
int main() {
Diamond d;
d.Derived1::x = 5;
d.Derived2::x = 10;
return 0;
}
3.2 The Solution (With Virtual Inheritance)
class Base {
public:
int x;
};
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
class Diamond : public Derived1, public Derived2 {};
int main() {
Diamond d;
d.x = 5;
return 0;
}
- With
virtual inheritance, Derived1 + Derived2 share single Base instance.
- Most-derived class (
Diamond) responsible for constructing the virtual base.
3.3 Trade-offs
- Adds level of indirection (pointer to shared base) → small runtime + memory overhead.
- Constructor ordering more complex — most-derived must explicitly init the virtual base.
- Use when you genuinely need diamond hierarchies; simplify design if possible.