C++ Virtual Functions


  • 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();  // Calls Base::foo(), NOT Derived::foo()
    }
    virtual void foo() {
        std::cout << "Base::foo()" << std::endl;
    }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    Derived() {
        foo();  // Calls Derived::foo() (Derived is now fully in scope)
    }
    void foo() override {
        std::cout << "Derived::foo()" << std::endl;
    }
};

int main() {
    Derived d;
    // Output:
    //   Base::foo()
    //   Derived::foo()
    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;
    // Output (correct, because destructor is virtual):
    //   Derived destructor
    //   Base destructor
    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:
  1. Ambiguity — referring to Base member through Diamond (e.g. d.x) ambiguous; compiler can't tell which copy.
  2. Wasted memoryBase 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.x = 5;          // ERROR: ambiguous -- which 'x'?
    d.Derived1::x = 5;   // Must disambiguate
    d.Derived2::x = 10;  // These are two separate 'x' members
    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;  // OK: only one copy of Base::x
    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

  1. Adds level of indirection (pointer to shared base) → small runtime + memory overhead.
  2. Constructor ordering more complex — most-derived must explicitly init the virtual base.
  3. Use when you genuinely need diamond hierarchies; simplify design if possible.