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 enable runtime polymorphism. When you call a virtual function through a base class pointer or reference, the actual derived class implementation is invoked based on the dynamic type of the object.

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

An important subtlety: virtual function calls inside constructors resolve to the current class's version, not the derived class's version. This is because during Base construction, the derived portion of the object has not been initialized yet, so the 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;
}

Similarly, calling a virtual function from the initialization list of a derived constructor will invoke the base class implementation, because the derived class has not yet been fully initialized.


2. Virtual Destructors

If a class is intended to be used as a base class (especially with polymorphic deletion through base pointers), its destructor must be virtual. Without a virtual destructor, deleting a derived object through a base pointer is undefined behavior and typically leaks the derived class'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: If a class has any virtual functions, give it a virtual destructor. If you are not sure, use virtual ~ClassName() = default;.


3. Virtual Inheritance (The Diamond Problem)

The "diamond problem" arises in multiple inheritance when a class inherits from two classes that themselves share a common base. The inheritance graph forms a literal diamond:

       Base
      /    \
Derived1   Derived2
      \    /
      Diamond

Diamond ends up with two copies of Base — one reached through Derived1, another through Derived2. This creates two problems:

  1. Ambiguity. Referring to a Base member through a Diamond object (for example d.x) is ambiguous because the compiler cannot tell which copy you mean.
  2. Wasted memory. The Base subobject is duplicated even when the design clearly wants one shared copy.

Virtual inheritance tells the compiler "all inheritance paths through this base should share a single subobject," resolving both issues.

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 and Derived2 share a single instance of Base. The most-derived class (Diamond) is responsible for constructing the virtual base class.

3.3 Trade-offs

  1. Virtual inheritance adds a level of indirection (a pointer to the shared base), so there is a small runtime and memory overhead.
  2. Constructor ordering becomes more complex: the most-derived class must explicitly initialize the virtual base.
  3. Use it when you genuinely need diamond-shaped hierarchies; avoid it if the design can be simplified.