Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

C++ Multiple Inheritance Memory Layout and the Necessity of Virtual Destructors

Tech May 17 2

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, and PrivateData2.

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 from CTEST (e.g., pointer arithmetic mismatch due to offset differences between CTEST* and the original CTest2*).

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++.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.