Senior 13 min · March 06, 2026

C++ Inheritance — Missing Virtual Destructor Leak

Game server memory grew monotonically; deleting via base pointer leaked textures & audio.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Inheritance lets a derived class reuse and extend base class members.
  • Public inheritance models IS-A; private inheritance is an implementation detail.
  • Virtual functions enable runtime polymorphism; always mark overrides with override.
  • The diamond problem requires virtual inheritance; it adds vptr overhead.
  • A missing virtual destructor in a polymorphic base causes silent resource leaks.
  • Calling virtual functions from constructors/destructors resolves to the base version — not the derived one.
✦ Definition~90s read
What is Inheritance in C++?

C++ inheritance without a virtual destructor is a memory corruption time bomb that silently leaks resources when you delete a derived object through a base class pointer. The problem is fundamental to C++'s object model: when delete is called on a base pointer, the compiler looks up the destructor statically unless it's virtual.

Think of inheritance like a family recipe book.

If the destructor isn't virtual, only the base class destructor runs—the derived class's destructor never executes, leaking any heap memory, file handles, or other resources the derived class allocated. This isn't a theoretical edge case; it's the #1 memory leak pattern in production C++ codebases, responsible for countless hours of debugging in systems ranging from game engines like Unreal to financial trading platforms.

Inheritance in C++ comes in five flavors: single, multiple, hierarchical, multilevel, and hybrid. But the access specifier—public, protected, or private—is what most developers get wrong. Public inheritance models an "is-a" relationship and is the only form that preserves polymorphism.

Protected and private inheritance model "implemented-in-terms-of" and break the Liskov substitution principle entirely; you cannot pass a privately-derived object where a base pointer is expected. This distinction is critical: misuse of private inheritance is a common source of confusing compilation errors and design smells.

Virtual functions are where inheritance earns its keep, enabling runtime polymorphism through the vtable (virtual table) and vpointer mechanism. Every class with virtual functions has a hidden vtable—a static array of function pointers—and each object carries a vpointer to that table.

When you call a virtual function through a base pointer, the compiler generates an indirect call through the vtable, dispatching to the correct derived implementation at runtime. This indirection costs one extra memory dereference per call but enables the polymorphic behavior that makes C++ suitable for large-scale frameworks like Qt, Boost, and LLVM.

Understanding this memory layout is essential for debugging, performance tuning, and avoiding the virtual destructor leak that silently corrupts your application's memory.

Plain-English First

Think of inheritance like a family recipe book. Grandma has a master recipe for 'cake' — she knows the basics: flour, eggs, sugar, oven temperature. Your mum inherits that recipe and adds chocolate chips. You inherit your mum's version and swap in oat flour. Nobody rewrote the whole book from scratch. That's exactly what inheritance does in C++ — a new class automatically gets everything a parent class already knows, and then adds its own twist on top.

Every large C++ codebase you'll ever work in — game engines, OS drivers, financial systems — uses inheritance somewhere. It's not a fancy academic trick. It's the practical answer to a real problem: when multiple things in your program share common behaviour, you shouldn't copy-paste that behaviour into each one. Code duplication is the silent killer of maintainability, and inheritance is one of the sharpest tools for defeating it.

Without inheritance, you'd write a 'save to disk' function separately for every class that needs it — User, Order, Product, Session. Change one detail of how saving works, and you're hunting through five files making the same edit. Inheritance lets you write that logic once in a base class and have every derived class pick it up automatically. It also enables polymorphism — the ability to treat different objects through a common interface — which is what makes plugin architectures, game entity systems, and GUI frameworks possible.

By the end of this article you'll be able to model real inheritance hierarchies yourself, understand the critical difference between public and private inheritance, dodge the two bugs that catch almost every intermediate C++ developer off guard, and answer the inheritance questions that come up in system-design interviews. Let's build something real.

Why Inheritance Without a Virtual Destructor Leaks

Inheritance in C++ lets a derived class reuse and extend the interface and implementation of a base class. The core mechanic is that a derived object contains a base class subobject, and access control, virtual functions, and destructor dispatch are all governed by the base class design. The critical detail: if you delete a derived object through a base pointer, the destructor must be virtual to invoke the derived destructor; otherwise, only the base destructor runs, leaking derived resources.

When you declare a base class with a non-virtual destructor and derive from it, the compiler generates a static call to the base destructor for any delete on a base pointer. This is correct only if the dynamic type matches the static type. In practice, polymorphic hierarchies (e.g., Shape* s = new Circle(...)) rely on virtual dispatch to call ~Circle(). Without virtual ~Shape(), the Circle destructor never executes, leaking heap memory, file handles, or other resources held by the derived part.

Use virtual destructors in any base class that is intended for polymorphic deletion — that is, any class with at least one virtual function. If a class is not meant to be deleted polymorphically (e.g., a mixin or a sealed hierarchy), mark it final or document the constraint. In real systems, this rule prevents silent resource leaks that are invisible in valgrind until the derived class adds a std::vector or a raw pointer.

Virtual Destructor Is Not Optional
A base class without a virtual destructor but with virtual functions is a design error — the language does not enforce it, but the resulting undefined behavior is a leak or crash.
Production Insight
A payment processing system used a Transaction base with virtual process() but a non-virtual destructor. When a RefundTransaction added a std::thread member, deleting through Transaction* leaked the thread handle, causing gradual descriptor exhaustion and process OOM after 10k refunds.
Symptom: steady memory growth with no single large allocation, only visible in /proc/pid/status as VmRSS climbing.
Rule of thumb: if a class has any virtual function, its destructor must be virtual — no exceptions.
Key Takeaway
A base class with virtual functions must have a virtual destructor — period.
Deleting a derived object through a base pointer without a virtual destructor is undefined behavior, not just a leak.
Mark classes not intended for polymorphic deletion as final to make the constraint explicit and enforceable.
C++ Inheritance: Virtual Destructor Leak THECODEFORGE.IO C++ Inheritance: Virtual Destructor Leak Flow from base class to memory leak without virtual destructor Base Class with Non-Virtual Destructor Missing virtual keyword on destructor Derived Class Allocation Derived object created via new Base Pointer to Derived Base* ptr = new Derived() delete via Base Pointer Calls base destructor only Derived Destructor Skipped Derived resources not released Memory Leak Derived part remains allocated ⚠ Missing virtual destructor in base class Always declare virtual destructor in polymorphic base classes THECODEFORGE.IO
thecodeforge.io
C++ Inheritance: Virtual Destructor Leak
Inheritance Cpp

Single Inheritance — Building Your First Base Class

Single inheritance is the simplest form: one class derives from exactly one parent. The derived class gets all public and protected members of the base class as if they were its own. Private members still exist in memory but are off-limits directly — they're the base class's internal secrets.

Why does the distinction between public, protected, and private members matter here? Because it defines the contract. Public members are the class's API — things it promises the outside world. Protected members are shared within the family but hidden from strangers. Private members belong to this class alone, full stop.

Here's a concrete scenario: you're building a simple vehicle management system. Every vehicle has a make, model, and a way to display its details. A Car adds the number of doors. Instead of writing the make/model logic twice, you put it in a base Vehicle class and let Car inherit it.

SingleInheritance.cppCPP
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
#include <iostream>
#include <string>

// Base class — contains everything common to ALL vehicles
class Vehicle {
public:
    std::string make;
    std::string model;
    int year;

    // Constructor: initialises the core vehicle data
    Vehicle(const std::string& make, const std::string& model, int year)
        : make(make), model(model), year(year) {}

    // Any vehicle can display its basic identity
    void displayInfo() const {
        std::cout << year << " " << make << " " << model;
    }
};

// Derived class — inherits Vehicle publicly, then extends it
class Car : public Vehicle {
public:
    int numberOfDoors;

    // We call Vehicle's constructor via the initialiser list
    // — the base part is constructed FIRST, always
    Car(const std::string& make, const std::string& model,
        int year, int doors)
        : Vehicle(make, model, year), numberOfDoors(doors) {}

    // Car-specific display — reuses the base method then adds its own info
    void displayCarInfo() const {
        displayInfo();  // inherited from Vehicle — no duplication needed
        std::cout << " | Doors: " << numberOfDoors << "\n";
    }
};

int main() {
    Vehicle genericVehicle("Generic", "Transport", 2020);
    genericVehicle.displayInfo();
    std::cout << "\n";

    Car familyCar("Toyota", "Camry", 2023, 4);
    familyCar.displayCarInfo();

    // Car inherits make/model/year directly — no extra code needed
    std::cout << "Make accessed directly: " << familyCar.make << "\n";

    return 0;
}
Output
2020 Generic Transport
2023 Toyota Camry | Doors: 4
Make accessed directly: Toyota
Pro Tip:
Always initialise the base class through the member initialiser list (: Vehicle(make, model, year)), never inside the constructor body. The base class constructor MUST run before any derived class logic, and the initialiser list is how you control which base constructor gets called and with which arguments.
Production Insight
When a derived class constructor does not explicitly call a base constructor, the compiler invokes the base's default constructor. If the base has no default constructor, the code won't compile. This catches many teams off guard.
Always verify that base classes intended for inheritance have a sensible default constructor or document which parameterised constructor must be called.
Key Takeaway
Base class must be fully constructed before derived class logic runs.
Always initialise the base through the member initialiser list, never the constructor body.
If the base has no default constructor, the derived class must explicitly call the parameterised one.

All Five Types of Inheritance in C++

C++ supports five forms of inheritance: single, multiple, multilevel, hierarchical, and hybrid. Understanding which one fits your problem is critical to keeping your codebase maintainable.

TypeDescriptionExample
SingleOne child inherits from exactly one parent.class Car : public Vehicle
MultipleOne child inherits from two or more direct parents.class SmartDevice : public Camera, public WiFi
MultilevelA chain: A → B → C, where C inherits B, B inherits A.class Mammal : public Animal; class Dog : public Mammal
HierarchicalOne parent is inherited by multiple children.class Vehicle { … }; class Car : public Vehicle; class Bike : public Vehicle
HybridCombination of multiple inheritance forms, often with a diamond.class A; class B : virtual A; class C : virtual A; class D : B, C
Production Insight
Hybrid inheritance almost always involves a diamond. In production, prefer composition over multiple inheritance when possible. If you must use multiple inheritance, ensure all bases except one are pure abstract (interfaces) to avoid the diamond and vptr overhead.
Key Takeaway
Know the five types: Single, Multiple, Multilevel, Hierarchical, Hybrid. Prefer shallow hierarchies and composition over deep or multiple inheritance.
Inheritance Types Overview
HybridRootLeftRightDerivedHierarchicalBaseChild1Child2MultilevelBaseIntermediateDerivedMultipleBase1DerivedBase2SingleBaseDerived

Public vs Protected vs Private Inheritance — The Bit Everyone Gets Wrong

When you write `class Car : public Vehicle, that public` keyword isn't about Vehicle's members — it's about how those members are re-exposed through Car to the outside world. This is the single most misunderstood concept in C++ inheritance, and getting it wrong leads to bizarre access errors.

Here's the mental model: the inheritance access specifier acts like a filter that can only make things more restrictive, never less. With public inheritance, public stays public and protected stays protected. With protected inheritance, public members become protected in the derived class. With private inheritance, everything from the base becomes private — completely locked away.

public inheritance models an IS-A relationship: a Car IS-A Vehicle. This is the form you'll use 90% of the time. private inheritance models HAS-A or IMPLEMENTED-IN-TERMS-OF — it's an implementation detail, not a conceptual relationship. If you catch yourself writing private inheritance, ask whether composition (just storing a member) would be cleaner.

InheritanceAccessDemo.cppCPP
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
#include <iostream>
#include <string>

class Engine {
public:
    void start() { std::cout << "Engine started\n"; }
    void stop()  { std::cout << "Engine stopped\n"; }
protected:
    int horsepower = 150;
private:
    std::string serialNumber = "ENG-XZ99"; // nobody outside Engine touches this
};

// PUBLIC inheritance — Car IS-A Engine (conceptually makes sense to expose API)
class PublicCar : public Engine {
public:
    void drive() {
        start();             // OK — public inherited, stays public
        std::cout << "Driving with " << horsepower << " HP\n"; // OK — protected
        // serialNumber;     // COMPILE ERROR — private is always off-limits
    }
};

// PRIVATE inheritance — ElectricBooster uses Engine internally
// The world outside ElectricBooster cannot call start() or stop()
class ElectricBooster : private Engine {
public:
    void activate() {
        start();  // OK inside the class — but outsiders can't call booster.start()
        std::cout << "Booster running at " << horsepower << " HP\n";
    }
};

int main() {
    PublicCar sportsCar;
    sportsCar.start();   // Fine — public inheritance keeps it public
    sportsCar.drive();
    sportsCar.stop();

    std::cout << "---\n";

    ElectricBooster booster;
    booster.activate();  // Fine — activate() is public
    // booster.start();  // COMPILE ERROR — start() is now private via private inheritance

    return 0;
}
Output
Engine started
Driving with 150 HP
Engine stopped
---
Engine started
Booster running at 150 HP
Watch Out:
The default inheritance in C++ is PRIVATE for class and PUBLIC for struct. If you write class Car : Vehicle (no specifier), it's private inheritance — which is almost never what you want. Always write the access specifier explicitly so your intent is clear and reviewers don't have to guess.
Production Insight
Mistaking private for public inheritance is a silent design error. Private inheritance exposes no interface, but still allows the derived class to access protected base members. This can lead to tightly coupled code that is hard to refactor.
When refactoring, check if : private Base can be replaced with composition. If the relationship is 'uses-internals', composition is almost always clearer.
Key Takeaway
Public inheritance = IS-A. Private inheritance = implemented-in-terms-of.
Default inheritance access is private for class and public for struct.
If you catch yourself using private inheritance, ask whether composition would be cleaner.

Virtual Functions and Polymorphism — Where Inheritance Gets Its Superpower

Inheriting data and functions is useful, but inheritance really earns its keep when you combine it with virtual functions. A virtual function says: 'I'm providing a default, but derived classes can replace me — and the RIGHT version will be called at runtime based on what the object actually is, not what pointer type you're using.'

This is runtime polymorphism — one of the core pillars of object-oriented design. Without it, you'd need a giant switch statement to check object types and call the right method. With it, you write code against the base class pointer and the system figures out which derived implementation to invoke automatically.

The virtual keyword on the base method opts into this mechanism. The override keyword on derived methods (C++11 onwards) makes the compiler confirm you're actually overriding something — it catches typos in function signatures that would otherwise silently create a new, unrelated function. Always use override. The = 0 syntax makes a function pure virtual, turning the class into an abstract base — it can't be instantiated and forces every derived class to provide an implementation.

PolymorphismDemo.cppCPP
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
85
86
87
#include <iostream>
#include <vector>
#include <memory>
#include <string>

// Abstract base class — cannot be instantiated directly
// Any class that forgets to implement fuelCost() won't compile
class Vehicle {
public:
    std::string make;
    int milesDriven;

    Vehicle(const std::string& make, int miles)
        : make(make), milesDriven(miles) {}

    // Pure virtual — every vehicle MUST define how fuel cost is calculated
    virtual double fuelCost() const = 0;

    // Virtual with a default — derived classes CAN override but don't have to
    virtual std::string fuelType() const {
        return "Unknown";
    }

    // Non-virtual: this behaviour should NEVER change per subtype
    void printSummary() const {
        std::cout << make
                  << " | Fuel: " << fuelType()
                  << " | Cost for " << milesDriven
                  << " miles: $" << fuelCost() << "\n";
    }

    // ALWAYS declare the destructor virtual in a polymorphic base class
    // (see Gotchas section — this one bites hard)
    virtual ~Vehicle() = default;
};

class PetrolCar : public Vehicle {
    double milesPerGallon;
    double pricePerGallon;
public:
    PetrolCar(const std::string& make, int miles,
              double mpg, double pricePerGallon)
        : Vehicle(make, miles),
          milesPerGallon(mpg),
          pricePerGallon(pricePerGallon) {}

    // override keyword tells the compiler: "check that I'm actually overriding"
    double fuelCost() const override {
        return (milesDriven / milesPerGallon) * pricePerGallon;
    }

    std::string fuelType() const override { return "Petrol"; }
};

class ElectricCar : public Vehicle {
    double milesPerKWh;
    double pricePerKWh;
public:
    ElectricCar(const std::string& make, int miles,
                double milesPerKWh, double pricePerKWh)
        : Vehicle(make, miles),
          milesPerKWh(milesPerKWh),
          pricePerKWh(pricePerKWh) {}

    double fuelCost() const override {
        return (milesDriven / milesPerKWh) * pricePerKWh;
    }

    std::string fuelType() const override { return "Electric"; }
};

int main() {
    // Store different vehicle types through a common base pointer
    // This is the power: the vector doesn't care which subtype it holds
    std::vector<std::unique_ptr<Vehicle>> fleet;

    fleet.push_back(std::make_unique<PetrolCar>("Ford F-150", 300, 20.0, 3.50));
    fleet.push_back(std::make_unique<ElectricCar>("Tesla Model 3", 300, 4.0, 0.13));
    fleet.push_back(std::make_unique<PetrolCar>("Honda Civic", 300, 35.0, 3.50));

    std::cout << "=== Fleet Cost Report ===\n";
    for (const auto& vehicle : fleet) {
        vehicle->printSummary(); // correct fuelCost() called automatically
    }

    return 0;
}
Output
=== Fleet Cost Report ===
Ford F-150 | Fuel: Petrol | Cost for 300 miles: $52.5
Tesla Model 3 | Fuel: Electric | Cost for 300 miles: $9.75
Honda Civic | Fuel: Petrol | Cost for 300 miles: $30
Interview Gold:
Interviewers love asking 'what is the difference between function overriding and function overloading?' Overloading is compile-time: same name, different parameters, resolved by the compiler. Overriding is runtime: same name, same signature, resolved at runtime via the vtable. They look similar but are completely different mechanisms.
Production Insight
Calling a virtual function from inside a constructor or destructor will NOT dispatch to the derived class version. The vtable during construction tracks the current class level only.
This causes subtle bugs: if a base constructor stores a callback that calls a virtual function, the callback later might point to the base version even after the object is fully constructed. Always document such patterns.
Key Takeaway
Always declare virtual functions with = 0 (pure virtual) or virtual in base and override in derived.
The compiler generates a vtable for each polymorphic class — one entry per virtual function.
Do not call virtual functions in constructors or destructors.

VTable and VPointer: Memory Layout of Virtual Functions

When a class has at least one virtual function, the compiler generates a static array called the virtual table (vtable). Each virtual function has a function pointer slot in this table. Every object of that class also gets a hidden vpointer (vptr) that points to its class's vtable.

Here's the memory layout: on the left is the object memory (stack/heap), which contains data members plus the vptr. The vptr points to the vtable, which stores pointers to the actual function implementations. When you call a virtual function through a base pointer, the generated assembly reads the vptr from the object, indexes into the vtable, and jumps to the correct derived implementation.

The diagram below shows the layout for a Derived object when Base has a virtual function foo() that Derived overrides:

Production Insight
The vptr adds 4/8 bytes per object (depending on architecture). In cache-sensitive code (game engines, high-frequency trading), this can degrade performance. If you have millions of small polymorphic objects, consider using std::variant or manual type dispatching to avoid vtable overhead.
Key Takeaway
Virtual dispatch adds one dereference: object → vptr → vtable → function pointer. This is fast but costs memory per object and prevents inlining.
VTable / VPointer Memory Layout
points toDerivedObject+vptr+int derivedDataVTable+Base::foo()* --> Derived::foo()+Base::<Base()* --> Derived::>Derived()

Rules for Virtual Functions in C++

Follow these rules to avoid common pitfalls when using virtual functions:

RuleExplanation
A virtual function must be a non-static member of a class.Free functions or static members cannot be virtual.
The base class function must be declared virtual.If not, the derived function hides instead of overrides.
The derived override must have an exactly matching signature.Different parameters or const-qualification create a new function, not an override. Always use override.
Virtual functions can be final in derived classes.Prevents further overriding. Useful for security or performance (devirtualisation).
Constructors cannot be virtual.That would require an already-constructed vtable to dispatch.
Destructors can (and should) be virtual in polymorphic bases.Ensures correct cleanup through base pointers.
Pure virtual functions (= 0) make the class abstract.The class cannot be instantiated; any derived class must implement all pure virtual functions to become concrete.
Virtual functions can have default arguments.But the default argument is determined by the static type, not the dynamic type. Avoid default arguments in virtual functions.
Calling a virtual function in a constructor/destructor uses the base version.The vtable is not fully formed until the derived constructor completes.
Production Insight
Default arguments in virtual functions are a common source of bugs. The compiler resolves them at compile time based on the pointer type, not the actual object type. Always specify arguments explicitly in the call to avoid confusion.
Key Takeaway
Use override on every derived virtual function. Avoid default arguments in virtual functions. Never call virtual functions from constructors/destructors.

Multiple Inheritance and the Diamond Problem — When Families Get Complicated

C++ allows a class to inherit from more than one base class. This is powerful but introduces a notorious hazard: the Diamond Problem. It occurs when two base classes both inherit from the same grandparent. Without a fix, the grandparent's data gets duplicated into the final class — two copies of the same members, ambiguous access, and logic bugs.

The fix is virtual inheritance on the intermediate classes. Virtual inheritance tells the compiler: 'even if multiple paths lead to this base, instantiate it exactly once.' The virtual base is then constructed by the most-derived class directly, which is why the most-derived constructor must call the virtual base constructor explicitly.

A practical rule: reach for multiple inheritance only when the extra base classes are pure interfaces (abstract classes with only pure virtual functions and no data). Mixing data-carrying base classes via multiple inheritance quickly turns into a maintenance nightmare — favour composition instead.

DiamondProblem.cppCPP
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
#include <iostream>
#include <string>

// Grandparent — the single shared root
class Device {
public:
    std::string deviceId;
    Device(const std::string& id) : deviceId(id) {
        std::cout << "Device constructed: " << id << "\n";
    }
    virtual ~Device() = default;
};

// virtual inheritance ensures only ONE Device sub-object exists
class NetworkDevice : virtual public Device {
public:
    NetworkDevice(const std::string& id) : Device(id) {}
    void sendPacket() { std::cout << deviceId << " sending packet\n"; }
};

class StorageDevice : virtual public Device {
public:
    StorageDevice(const std::string& id) : Device(id) {}
    void writeData() { std::cout << deviceId << " writing data\n"; }
};

// SmartRouter inherits from BOTH — classic diamond shape
// Because both parents used virtual inheritance, Device is constructed ONCE
// The most-derived class (SmartRouter) MUST call Device's constructor directly
class SmartRouter : public NetworkDevice, public StorageDevice {
public:
    SmartRouter(const std::string& id)
        : Device(id),            // required: most-derived calls virtual base
          NetworkDevice(id),
          StorageDevice(id) {}

    void status() {
        // No ambiguity — there's only one deviceId
        std::cout << "Router " << deviceId << " is operational\n";
    }
};

int main() {
    std::cout << "--- Without virtual inheritance, Device would construct TWICE ---\n";
    std::cout << "--- With virtual inheritance, it constructs exactly once:      ---\n\n";

    SmartRouter router("RTR-001");
    router.sendPacket();
    router.writeData();
    router.status();

    return 0;
}
Output
--- Without virtual inheritance, Device would construct TWICE ---
--- With virtual inheritance, it constructs exactly once: ---
Device constructed: RTR-001
RTR-001 sending packet
RTR-001 writing data
Router RTR-001 is operational
Watch Out:
Virtual inheritance adds overhead — the compiler uses an extra pointer (vptr) to locate the single shared base. For hot paths in performance-critical code (game loops, audio callbacks), measure before committing to deep virtual inheritance hierarchies. If both bases are pure abstract interfaces with no data, there's no diamond to worry about at all.
Production Insight
Virtual inheritance adds a level of indirection (vptr) to every access of the virtual base's data members. In performance-critical code (e.g., game loop), this can cause cache misses and measurable slowdown. Profile before using virtual inheritance on a hot path.
Also, the most-derived class must explicitly initialise the virtual base, which adds constructor complexity and risks missing initialisations.
Key Takeaway
Virtual inheritance solves the diamond by constructing the shared base exactly once.
The most-derived class is responsible for calling the virtual base constructor.
Performance trade-off: extra vptr overhead. Prefer using pure abstract interfaces as intermediate bases to avoid diamond complications entirely.

Inheritance vs Composition — The Real Trade-off

One of the most common design debates is whether to model a relationship with inheritance or composition. The rule of thumb: inheritance is for polymorphic substitution (you need to treat objects through a common interface), composition is for code reuse (you just need the functionality). In production, composition almost always wins for reuse because it's less coupled, easier to test, and doesn't force you into a class hierarchy. However, inheritance is indispensable when you need to build plugin systems, framework callbacks, or any code that must operate on unknown types.

Here's a concrete example: instead of inheriting from a Logger to reuse logging behaviour, store a Logger member and delegate to it. This lets you swap loggers at runtime (file, network, mock) without changing the class hierarchy.

CompositionVsInheritance.cppCPP
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
#include <iostream>
#include <memory>
#include <string>

// ---- Inheritance approach ----
class Logger {
public:
    virtual void log(const std::string& msg) const {
        std::cout << "[Inheritance] " << msg << "\n";
    }
    virtual ~Logger() = default;
};

class ServiceWithInheritance : public Logger {
public:
    void doWork() {
        // 'log' is inherited, but the relationship is now IS-A Logger
        log("Working...");
    }
};

// ---- Composition approach ----
class ServiceWithComposition {
    std::shared_ptr<Logger> logger_; // can be any Logger implementation
public:
    explicit ServiceWithComposition(std::shared_ptr<Logger> logger)
        : logger_(std::move(logger)) {}
    void doWork() {
        // delegation: uses logger, not inherits
        logger_->log("Working...");
    }
};

class FileLogger : public Logger {
public:
    void log(const std::string& msg) const override {
        std::cout << "[File] " << msg << "\n";  // pretend file I/O
    }
};

int main() {
    // Inheritance: fixes the Logger type at compile time
    ServiceWithInheritance svc_inherit;
    svc_inherit.doWork();

    // Composition: can swap logger at runtime
    auto fileLogger = std::make_shared<FileLogger>();
    ServiceWithComposition svc_compose(fileLogger);
    svc_compose.doWork();

    // Composition also works with the default Logger
    auto defaultLogger = std::make_shared<Logger>();
    ServiceWithComposition svc_compose2(defaultLogger);
    svc_compose2.doWork();

    return 0;
}
Output
[Inheritance] Working...
[File] Working...
[Inheritance] Working...
Mental Model: Inheritance is a contract, composition is a tool
  • IS-A relationships need public inheritance (e.g., Car IS-A Vehicle).
  • HAS-A or USES-A relationships need composition (e.g., Service HAS-A Logger).
  • Deep inheritance hierarchies (>3 levels) are brittle and hard to debug.
  • Composition allows runtime polymorphism via interfaces without coupling the class hierarchy.
  • If you find yourself writing 'protected' members to share implementation, consider composition instead.
Production Insight
Deep inheritance hierarchies (5+ levels) become impossible to trace during debugging. I've spent days chasing a bug that turned out to be a virtual function call resolving to a base 4 levels up.
Prefer shallow hierarchies (2-3 levels max) with composition for behavior reuse. The Liskov substitution principle is your litmus test: if a derived class cannot truly replace its base without changing the program's correctness, composition is the answer.
Key Takeaway
Favor composition over inheritance for code reuse.
Use inheritance only when you need polymorphic substitution (Liskov substitution).
Deep hierarchies are a maintenance trap — keep inheritance trees shallow.
When to Use Inheritance vs Composition
IfNeed polymorphic substitution (pass derived as base)?
UseUse public inheritance.
IfNeed to reuse implementation without exposing interface?
UseUse composition or private inheritance (prefer composition).
IfNeed to reuse interface only (no implementation)?
UseUse inheritance from a pure abstract base (interface).
IfMultiple child classes share only behavior, not identity?
UseUse composition: store a shared member or inject via constructor.

Advantages and Disadvantages of Inheritance in C++

Inheritance is a powerful tool, but like any tool it has trade-offs. Use this table to decide when to lean in and when to pull back.

AdvantagesDisadvantages
Code reuse: Base class logic is automatically available to all derived classes.Tight coupling: Derived classes are tightly coupled to base class internals. Changes in the base can break all derived classes.
Polymorphism: Enables runtime dispatch through virtual functions, making plugins and flexible designs possible.Fragile base class problem: Adding a new virtual function to a base can cause unexpected behaviour in derived classes.
Logical hierarchy: Models real-world IS-A relationships clearly.Deep hierarchies are hard to maintain: 5+ levels of inheritance make debugging and understanding code difficult.
Extensibility: New derived classes can add functionality without modifying existing code (open/closed principle).Multiple inheritance complexity: Diamond problem and virtual inheritance add hidden overhead and mental burden.
Interface contracts: Abstract bases force derived classes to implement required methods.Object slicing: Passing by value loses derived data; requires careful use of pointers/references.
Standard library integration: Many STL containers and algorithms work seamlessly with polymorphic objects.Performance overhead: Virtual dispatch adds indirection (vptr, vtable lookup) that prevents inlining and can cause cache misses.

In production, use inheritance sparingly. When you do use it, keep the hierarchy shallow, mark overrides with override, and always consider whether composition would serve better.

Production Insight
Teams often over-engineer with deep inheritance trees. I've seen a 7-level hierarchy that took a week to refactor into a 2-level tree with composition. The result was easier to test and 30% faster in hot paths. Measure before you design.
Key Takeaway
Inheritance gives reuse and polymorphism at the cost of coupling and complexity. Prefer shallow hierarchies and composition where possible.

Practice Problems: C++ Inheritance

Apply what you've learned with these hands-on problems. Focus on understanding the concepts rather than memorising syntax.

Problem 1: Virtual Destructor Detection Given the following code, will there be a memory leak? If so, why? ``cpp class Base { public: ~Base() {} }; class Derived : public Base { int data = new int[100]; }; int main() { Base p = new Derived(); delete p; } ` Answer: Yes – leak. Base destructor is non-virtual, so ~Derived() never runs. Fix: add virtual ~Base() = default;`.

Problem 2: Multiple Inheritance Ambiguity ``cpp struct A { void foo() {} }; struct B : A {}; struct C : A {}; struct D : B, C {}; int main() { D d; d.foo(); } ` Does this compile? If not, what's the fix? Answer: No – ambiguous call to A::foo(). Fix: use virtual inheritance (class B : virtual public A, class C : virtual public A`).

Problem 3: Override vs Overload ``cpp class Base { public: virtual void print(int x) { std::cout << "Base"; } }; class Derived : public Base { public: void print(double x) { std::cout << "Derived"; } }; ` When calling Derived d; d.print(5), which version runs? Answer: Derived version because int converts to double. The function print(double) is a new function (overload), not an override. Add override` to catch this mistake. If you want to override, match signature exactly.

Problem 4: Diamond Problem without Virtual Inheritance Given the diamond shape with a data member int value in the root, how many copies of value exist in the most-derived class? Answer: Two copies – one from each path. Fix: use virtual inheritance on intermediate classes.

Problem 5: Virtual Table Construction How many vtable entries does a class with three virtual functions and no overrides have? Answer: Three entries (one per virtual function). Each derived class that overrides any of them gets its own vtable with updated pointers.

Production Insight
These problems mirror real code review scenarios. In production, catching missing virtual destructors or ambiguous inheritance during code review saves debugging time. Use compiler warnings (-Wnon-virtual-dtor) and static analysis tools to automate detection.
Key Takeaway
Practice detecting common inheritance bugs: missing virtual destructor, ambiguous base, overload vs override confusion, and diamond duplicates.

Slicing: The Silent Data Loss That Kills Polymorphism

You pass an object by value to a function expecting a base class. Compiles fine. But the derived part? Gone. Sliced off. This is C++'s version of a silent type cast that discards everything you just inherited.

The root cause is how C++ handles value semantics. When you copy a Derived object into a Base parameter, the compiler only copies the Base portion. The compiler doesn't know — and doesn't care — about the Derived's virtual table pointer or extra members. It just memcpy's the base part. Polymorphism? Dead. Destructor? Base's version runs. You just lost any extended state and behavior.

Senior teams enforce a rule: never pass polymorphic objects by value. Always use pointers or references. If someone checks in code with void process(SensorData data), you flag it. Because that function will happily slice every derived sensor type you throw at it, and nobody will notice until the temperature readings stop making sense.

SensorSlicing.cppCPP
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
// io.thecodeforge — c-cpp tutorial

#include <iostream>
class SensorData {
public:
    virtual void report() { std::cout << "Base sensor\n"; }
    virtual ~SensorData() = default;
};

class TempSensor : public SensorData {
    double temperature = 45.6;
public:
    void report() override { std::cout << "Temp: " << temperature << "\n"; }
};

void logSensor(SensorData data) {  // BAD: slices off temperature
    data.report();                  // calls Base::report, not TempSensor::report
}

int main() {
    TempSensor ts;
    logSensor(ts);
    SensorData* ptr = &ts;
    ptr->report();                  // calls TempSensor::report via vtable
}
Output
Base sensor
Temp: 45.6
Production Trap:
Slicing doesn't just lose virtual dispatch — it corrupts data silently. If your derived class has a raw pointer managing memory, slicing leaks that memory because the base destructor never sees it.
Key Takeaway
Pass polymorphic objects by pointer or reference, never by value.

Virtual Inheritance: Why Your Diamond Won't Kill You If You Do It Right

Everyone fears the diamond problem. Two base classes, both deriving from the same grandparent, a single derived class inheriting both. Without virtual inheritance, you get two copies of the grandparent. Ambiguity, state duplication, and headaches at the water cooler.

The fix: virtual keyword on the inheritance specifier. But here's what the tutorials won't tell you — virtual inheritance changes the memory layout and construction order drastically. The most derived class becomes responsible for constructing the virtual base. Not the intermediate classes. So if a base class in the middle tries to initialize the virtual base, it gets silently ignored. This has cost teams days of debugging.

Second pain: casting. You can't static_cast from a virtual base to a derived class. The compiler doesn't know where the derived class sits relative to the virtual base at compile time. You need dynamic_cast or a C-style cast (don't). If you have virtual inheritance, expect RTTI overhead and slightly slower cast operations.

Third: construction order. Virtual bases are always constructed first, in depth-first left-to-right order of the inheritance graph. If your virtual base constructor throws, the entire object hierarchy collapses. Protect that constructor like it's hosting the king.

DiamondConstruction.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — c-cpp tutorial

#include <iostream>
struct Logger {
    Logger() { std::cout << "Logger ctor\n"; }
};
struct Networked : virtual public Logger {
    Networked() { std::cout << "Networked ctor\n"; }
};
struct FileBased : virtual public Logger {
    FileBased() { std::cout << "FileBased ctor\n"; }
};
struct HybridLogger : public Networked, public FileBased {
    HybridLogger() { std::cout << "HybridLogger ctor\n"; }
};

int main() {
    HybridLogger hl;
}
Output
Logger ctor
Networked ctor
FileBased ctor
HybridLogger ctor
Senior Shortcut:
If you're using virtual inheritance, you probably need composition instead. Virtual inheritance is a strong design smell. Only reach for it when you truly need shared state across a diamond — typically in middleware or plugin architectures.
Key Takeaway
Virtual inheritance is for the most derived class to own — intermediate classes are just passengers.
● Production incidentPOST-MORTEMseverity: high

Missing Virtual Destructor Causes Silent Resource Leak in Game Engine

Symptom
Game server memory usage grew monotonically. After player disconnections, memory was not reclaimed. The leak only occurred when deleting polymorphic objects via base class pointers.
Assumption
The team assumed that C++ automatically calls the most-derived destructor, just like it calls the most-derived virtual function.
Root cause
Base class GameObject had virtual functions but its destructor was non-virtual. Deleting through GameObject* called only ~GameObject(), skipping the derived class destructor that freed OpenGL textures and audio buffers.
Fix
Declared virtual ~GameObject() = default; in the base class.
Key lesson
  • Always add a virtual destructor if a class has at least one virtual function.
  • The destructor is the one function that must be virtual in polymorphic bases.
  • Tooling: Valgrind and ASan can catch such leaks during development.
Production debug guideQuick symptom-to-root-cause mapping for common inheritance bugs5 entries
Symptom · 01
Derived class function is never called (base version runs instead)
Fix
Check function signature match — missing const, different parameter types, or missing override keyword.
Symptom · 02
Memory leak when deleting via base pointer
Fix
Verify base class destructor is virtual. Use valgrind or -fsanitize=address to confirm which destructors run.
Symptom · 03
Compile error: cannot instantiate abstract class
Fix
Identify all pure virtual functions in the class hierarchy. Ensure derived class provides implementations for each.
Symptom · 04
Ambiguous member access (error: request for member is ambiguous)
Fix
Check for diamond inheritance. Add virtual inheritance on middle classes. Qualify member with base class name.
Symptom · 05
Object slicing: derived data lost when assigning to base by value
Fix
Avoid passing objects by value in polymorphic contexts. Use pointers/references and prevent slicing.
★ C++ Inheritance Debugging Cheat SheetQuick commands and checks for diagnosing inheritance problems in production code.
Virtual function not called correctly
Immediate action
Add `override` keyword and recompile to catch mismatch.
Commands
g++ -std=c++20 -Wall -Wextra -Wpedantic -Wnon-virtual-dtor
nm -C binary | grep vtable
Fix now
Ensure function signatures match exactly (include const). Use final to prevent unexpected overrides.
Memory leak in polymorphic class hierarchy+
Immediate action
Check if base destructor is virtual.
Commands
grep -r 'virtual.*~' include/ | grep -v '= default'
valgrind --leak-check=full ./app
Fix now
Add virtual ~Base() = default; to the base class.
Compiler error: ambiguous base class member+
Immediate action
Identify the diamond shape in your hierarchy.
Commands
g++ -E -P source.cpp | grep 'class.*:'
pahole binary | grep -A5 'class'
Fix now
Add virtual to the inheritance of the middle classes from the common base.
Object slicing (extra derived data lost)+
Immediate action
Check if you're passing objects by value.
Commands
grep -rn 'void.*(' src/ | grep -v '*'
clang-tidy --checks=readability-slicing
Fix now
Change function parameter to accept a pointer or reference.
Public vs Private Inheritance
AspectPublic InheritancePrivate Inheritance
Relationship modelledIS-A (Car is a Vehicle)IMPLEMENTED-IN-TERMS-OF (uses internals)
Base public members in derivedRemain publicBecome private
Base protected members in derivedRemain protectedBecome private
Accessible outside the derived classYes — full base API exposedNo — base API is hidden
Pointer/reference substitution (Liskov)Allowed — derived converts to baseNot allowed — compiler error
Typical use casePolymorphic hierarchies, interfacesRarely used; prefer composition
Preferred alternative when overusedStore base as a private member instead

Key takeaways

1
The inheritance access specifier (public/protected/private) controls how the base class's API is re-exposed through the derived class
public inheritance models IS-A; private inheritance models implemented-in-terms-of and is almost always better replaced by composition.
2
Always use the override keyword when overriding a virtual function
it turns a silent logic bug (mismatched signature creates a new function) into a compile-time error you can fix immediately.
3
Always declare a virtual destructor in any base class with virtual functions
skipping it causes derived destructors to be silently skipped when deleting through a base pointer, leaking resources with no warning.
4
The Diamond Problem is solved by virtual inheritance on the middle classes, but the most-derived class must then call the virtual base constructor directly
and virtual inheritance adds a vptr cost, so prefer pure-abstract interfaces to avoid the issue entirely.
5
Favor composition over inheritance for code reuse
deep hierarchies are hard to debug and maintain.

Common mistakes to avoid

4 patterns
×

Forgetting a virtual destructor in the base class

Symptom
When deleting a derived object through a base pointer, only the base destructor executes. Derived class resources (memory, file handles, network connections) are never freed, causing silent resource leaks.
Fix
Declare virtual ~BaseClass() = default; in any base class that has virtual functions or is intended for polymorphic deletion.
×

Accidentally creating a new function instead of overriding because of a mismatched signature

Symptom
The derived function never runs when called polymorphically. Code compiles without warning if override is missing, and the base version is silently invoked.
Fix
Always add the override keyword after the function declaration. The compiler will produce an error if the signature does not exactly match a base class virtual function.
×

Calling a virtual function from a constructor or destructor

Symptom
The function call resolves to the current class version, not the most-derived version. This often causes unexpected behavior or crashes if the derived class expects its own implementation to run during construction.
Fix
Never call virtual functions in constructors or destructors. Use a separate init() method called after full construction, or restructure to avoid the need.
×

Object slicing by passing polymorphic objects by value

Symptom
The derived part of the object is sliced off when passed by value to a function expecting a base class. Only the base portion remains; virtual dispatch still works via the vptr, but derived data members are lost.
Fix
Always pass polymorphic objects by pointer or reference. Use const Base& for read-only access. If you must copy, implement a virtual clone() method.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between virtual and non-virtual inheritance in C+...
Q02SENIOR
Why must you declare a destructor as virtual in a polymorphic base class...
Q03SENIOR
If a derived class doesn't explicitly call the base class constructor in...
Q04JUNIOR
What is the difference between function overloading and function overrid...
Q05SENIOR
Explain the diamond problem and how virtual inheritance solves it. What ...
Q01 of 05SENIOR

What is the difference between virtual and non-virtual inheritance in C++, and when would you actually use virtual inheritance?

ANSWER
Virtual inheritance ensures that a base class sub-object is shared (only one instance) when it appears multiple times in a class hierarchy (e.g., classic diamond problem). Non-virtual inheritance creates separate copies of the base class for each derived branch. Use virtual inheritance when you have diamond-shaped multiple inheritance and want to avoid ambiguous duplicate members. However, it adds vptr overhead; often better to redesign to use pure abstract interfaces (no data) on the intermediate bases.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between inheritance and composition in C++?
02
Can a C++ class inherit from multiple base classes at the same time?
03
What does it mean for a class to be abstract in C++, and how do you create one?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C++ Basics. Mark it forged?

13 min read · try the examples if you haven't

Previous
Constructors and Destructors in C++
5 / 19 · C++ Basics
Next
Polymorphism in C++