2Brute2Best
πŸ”₯0
M3.3

Virtual Destructors, Object Slicing & override / final

Medium High-frequency interview topic Pro Onlyvirtual ~Base()Object Slicingoverride & finalName Hiding (using Base::fn)
⚑ 15-Second Mental ModelM3.3 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Core Intuition)

In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.

Rule to Remember: Make illegal states impossible to compile or run.
3 Golden Facts to Remember
Fact #1
Why does the C++ standard make deleting via a non-virtual base pointer Undefined Behavior rather than just skipping the derived destructor?

Because in addition to skipping Derived::~Derived(), the compiler may not know the correct memory size or exact base offset to pass to the memory deallocator (operator delete). Under multiple inheritance, Base* might point to an offset within Derived. Without a virtual destructor to perform pointer adjustment back to the start of the allocated block, passing the unadjusted offset to free() crashes the heap manager.

Fact #2
Why does making a base destructor protected and non-virtual provide both performance and safety benefits?

A protected destructor prevents client code from writing delete basePtr; at compile time, eliminating the risk of undefined polymorphic deletion. Because the destructor is non-virtual, the class does not require an 8-byte __vptr or a vtable, keeping the struct standard-layout with zero memory overhead.

Fact #3
How does the virtual clone() method (Prototype Pattern) solve the 'Virtual Constructor' dilemma?

Constructors cannot be virtual because an object must exist before virtual dispatch can function. A pure virtual std::unique_ptr<Base> clone() const = 0; allows polymorphic copying: calling ptr->clone() dispatches via the dynamic type's vtable, invoking the derived class's copy constructor and returning a new heap copy without callers knowing the concrete derived type.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Interviewers test virtual destructors and slicing to determine whether you write leak-prone code or memory-safe systems:

  • Deletion Safety: Explaining why deleting via a non-virtual base pointer is Undefined Behavior ([expr.delete]/3) that skips derived destructors and corrupts heap allocator sizing.
  • The Sutter Destructor Rule: Knowing when a base destructor must be public virtual (polymorphic ownership) versus protected non-virtual (zero-overhead mixins/policies).
  • Object Slicing: Tracing how passing polymorphic objects by value chops off derived fields and overwrites __vptr with the base vtable.
  • Contract Enforcement: Using override to catch silent signature drift at compile time and final to unlock compiler devirtualization.
02

Under the hood & memory layout

Hardware & memory representation

Destruction and copying have exact physical consequences in polymorphic memory layouts:

  1. Deleting via Base Pointer (delete basePtr;):
    • Without virtual: The compiler uses static dispatch, jumping directly to Base::~Base(). The derived destructor Derived::~Derived() never runs, leaking derived heap buffers, open sockets, and file handles. This is Undefined Behavior ([expr.delete]/3).
    • With virtual: VTable slot 1 holds a deleting destructor thunk. It invokes Derived::~Derived(), unwinds ancestor destructors, and calls operator delete with the correct total byte size (sizeof(Derived)).
  2. Object Slicing (Base b = derivedObj;):
    • Memory Truncation: A Base stack slot has space only for sizeof(Base) bytes. Passing Derived by value physically chops off all trailing derived members.
    • VTable Rewriting: The copy constructor initializes b's __vptr with Base's vtable. The sliced object loses its derived identity forever.
Virtual Destructor VTable Slot (Deleting Dtor)

Under the Itanium C++ ABI, declaring a virtual destructor adds two slots to the vtable: the complete object destructor (~Derived()) and the deleting destructor which runs ~Derived() and then invokes operator delete(sizeof(Derived)).

Memory Slicing Truncation

Copying a 48-byte Derived into a 16-byte Base value truncates the trailing 32 derived bytes. The stack frame of the receiving function allocates only 16 bytes for Base.

vptr Rewriting During Slicing

In Base b = derivedObj;, b's __vptr is initialized with &Base_vtable, not &Derived_vtable. Subsequent virtual function calls on b invoke Base methods.

Protected Destructor (0 Bytes Overhead)

A base class with protected ~Base() = default; has no vptr (0 bytes overhead) and causes delete basePtr; to fail at compile time, guaranteeing deletion safety without runtime cost.

03

Core concepts & syntax traps

01

Why 'delete basePtr;' Without 'virtual ~Base()' Is Undefined Behavior

Deleting an object through a base pointer without a virtual destructor triggers catastrophic bugs:

  1. Derived Destructor Is Skipped: The compiler generates a direct, static jump to Base::~Base(). The derived destructor Derived::~Derived() never executes, instantly leaking derived heap allocations, file handles, and mutex locks.
  2. Heap Memory Corruption: The memory allocator expects operator delete to receive the original allocation size. If it receives sizeof(Base) instead of sizeof(Derived), heap binning metadata becomes corrupted.
  3. Undefined Behavior: The C++ standard explicitly classifies this mismatch as Undefined Behavior ([expr.delete]/3). Any polymorphic base class must declare a virtual destructor.
C++23 snippet
class LeakyBase {
public:
    ~LeakyBase() = default; // BUG: Non-virtual destructor!
};
class HeavyDerived : public LeakyBase {
    std::vector<int> hugeBuffer_{1'000'000, 42};
public:
    ~HeavyDerived() { /* never runs if deleted via LeakyBase*! */ }
};

LeakyBase* p = new HeavyDerived();
delete p; // UNDEFINED BEHAVIOR: hugeBuffer_ is leaked, allocator corrupted!
Interview takeaway:

Any class with at least one virtual function must declare a public virtual destructor.

02

The Golden Rule: Base Destructor Must Be Public Virtual OR Protected Non-Virtual

The C++ Core Guidelines establish a strict design rule for base class destructors:

  • Public and Virtual (Polymorphic Bases):
    • When to use: Callers manage derived instances through base pointers (std::unique_ptr<Base>).
    • Cost: Adds an 8-byte __vptr to each object, but guarantees safe cleanup through the deleting destructor.
  • Protected and Non-Virtual (Policy/Mixin Classes):
    • When to use: The base class exists purely to share code or methods, and callers should never delete objects through a Base*.
    • Benefit: Writing delete basePtr; triggers a compile-time error because ~Base() is inaccessible. You get 100% deletion safety with 0 bytes of memory overhead.
C++23 snippet
// MIXIN BASE: Zero-overhead, deletion-safe
class NonPolymorphicPolicy {
protected:
    ~NonPolymorphicPolicy() = default; // Protected: cannot be deleted via Base*
public:
    void logAction() const { /* ... */ }
};

class Worker : public NonPolymorphicPolicy {};

Worker* w = new Worker();
// NonPolymorphicPolicy* p = w;
// delete p; // COMPILE ERROR: ~NonPolymorphicPolicy is protected!
delete w;   // SAFE: Worker destructor is public
Interview takeaway:

Make base destructors public and virtual for polymorphic hierarchies, or protected and non-virtual for non-polymorphic mixins.

03

Object Slicing in Memory: Why Value Containers (vector<Base>) Fail

Passing or storing polymorphic objects by value triggers Object Slicing:

  1. Memory Truncation: A function taking Base b allocates a stack slot of exactly sizeof(Base). If passed a 48-byte Derived object, the compiler copies only the first 16 bytes. The trailing 32 bytes of derived data are physically sliced off.
  2. VTable Overwrite: The receiving object b is constructed as a genuine Base. Its __vptr is set to point to Base's vtable, completely erasing any derived virtual function overrides.
  3. The Fix: Never create containers of values (std::vector<Base>). Always store smart pointers (std::vector<std::unique_ptr<Base>>) or pass references (const Base&) to preserve the full dynamic type.
C++23 snippet
class Base {
public:
    virtual void speak() const { std::println("Base"); }
};
class Derived : public Base {
public:
    int extra_{99};
    void speak() const override { std::println("Derived extra={}", extra_); }
};

Derived d;
Base sliced = d; // OBJECT SLICING: extra_ is discarded!
sliced.speak();  // Prints 'Base' (vptr points to Base_vtable)
Interview takeaway:

Never pass polymorphic objects by value or store them by value in containers; pass const Base& or store std::unique_ptr<Base>.

04

C++11 override & final: Eliminating Silent Overload Bugs & Devirtualization

Prior to C++11, typos in virtual function signatures caused silent bugs:

  • override (Compile-time contract check):
    • Problem: Missing a const or writing unsigned int instead of int silently created an entirely new function instead of overriding the base method.
    • Solution: Adding override instructs the compiler to halt compilation if the method fails to match an exact base virtual signature.
  • final (Optimizer accelerator):
    • Contract: Prevents further inheritance on a class (class Leaf final : public Base) or overriding of a method.
    • Optimization: Tells the compiler that no further derived types can ever exist, allowing it to perform devirtualizationβ€”turning indirect vtable lookups into direct, inlinable calls.
C++23 snippet
class BaseDevice {
public:
    virtual void calibrate(int level) const = 0;
};
class PressureSensor : public BaseDevice {
public:
    // void calibrate(int level) override; // COMPILE ERROR: missing 'const'!
    void calibrate(int level) const override final { /* Devirtualized by compiler! */ }
};
Interview takeaway:

Always mark virtual overrides with override; mark leaf classes or non-overridable methods final to enable devirtualization.

05

Name Hiding Across Scopes & Resolution with 'using Base::fn'

In C++, name lookup happens before overload resolution:

  1. Scope Search Halts Early: The compiler looks for function names from the derived scope upward. The moment it finds a matching identifier name in Derived, it stops searchingβ€”completely hiding base overloads with the same name, even if their argument types differ.
  2. The Bug: If Base provides compute(double) and Derived declares compute(int), calling d.compute(3.14) does not call the base version. It silently truncates 3.14 to 3 and calls Derived::compute(int).
  3. The Fix (using Base::fn;): Declare using Base::compute; inside the derived class body. This pulls all base overloads into the derived scope so they participate in normal overload resolution.
C++23 snippet
class BaseCalculator {
public:
    void compute(double d); // Overload 1
};
class DerivedCalculator : public BaseCalculator {
public:
    using BaseCalculator::compute; // Brings BaseCalculator::compute into derived scope!
    void compute(int i);           // Overload 2
};

DerivedCalculator calc;
calc.compute(3.14); // Correctly invokes compute(double), not compute(int)!
Interview takeaway:

When overloading an inherited function name in a derived class, add 'using Base::fn;' to prevent name hiding.

Brute2Best Pro Content

Unlock Live Implementation & Interview Scenarios

The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering