C++ Multiple Inheritance Memory Layout and the Necessity of Virtual Destructors
Class Hierarchy and Memory Layout
Consider the following class definitions:
class CBase {
public:
int Data;
CBase() = default;
~CBase();
};
class CTEST {
private:
int PrivateData1;
int PrivateData2;
public:
int Data;
CTEST() = default;
~CTEST();
void PrintData();
};
class CTest2 : public CBase, public CTEST {
public:
int Data;
CTest2() = default;
~CTest2();
void PrintData2();
private:
int PrivateData1;
int PrivateData2;
};
Note that CTest2 inherits from both CBase and CTEST, and redeclares Data in each scope. These are distinct members — no shadowing or overriding occurs at the data member level.
Size Analysis
When instantiated, CTest2 contains all members from both base classes plus its own declared members. Its size is the sum of:
sizeof(CBase)(including padding),sizeof(CTEST)(including its private and public fields),- plus its own
int Data,PrivateData1, andPrivateData2.
This reflects standard C++ object layout for multiple inheirtance: subobjects are laid out sequentially, with possible padding for alignment.
Example inspection:
CTest2* instance = new CTest2();
std::cout << "CTEST size: " << sizeof(CTEST) << '\n';
std::cout << "CTest2 size: " << sizeof(CTest2) << '\n';
CTEST* base_ptr = instance; // Upcast to first base (CTEST)
delete base_ptr; // Dangerous without virtual destructor
Consequences of Non-Virtual Destructors
If ~CTEST() is not declared virtual, deleting via a CTEST* pointing to a CTest2 object results in:
- Only
~CTEST()being invoked, ~CBase()and~CTest2()never called,- Potential resource leaks (e.g., un-freed memory, unclosed handles),
- Undefined behavior if
CTest2’s layout differs significantly fromCTEST(e.g., pointer arithmetic mismatch due to offset differences betweenCTEST*and the originalCTest2*).
This happens because static dispatch resolves the destructor call at compile time based solely on the static type (CTEST*), not the dynamic type (CTest2*).
Resolution: Declare Base Class Destructors Virtual
To ensure proper cleanup across the hierarchy, declare destructors virtual in any base class intended for polymorphic use:
class CTEST {
// ... other members ...
public:
virtual ~CTEST(); // Enables dynamic dispatch
};
class CBase {
public:
virtual ~CBase(); // Also virtual for safety if used polymorphically
};
With this change, delete base_ptr triggers the full destructor chain: ~CTest2() → ~CTEST() → ~CBase(), in reverse order of construction.
A class designed as a base for inheritance must declare its destructor virtual, evenif empty — it’s a fundamental rule for safe polymorphism in C++.