The virtual keyword

C++
Author

Quasar

Published

May 22, 2025

virtual functions

The virtual keyword specifies that a non-static member function is virtual and supports dynamic dispatch. It may only appear in the initial declaration of a non-static member function (i.e., when it is declared in the class definition).

#include <iostream>

class Base
{
    public:
    void foo(){
        std::cout << "\n" << "Base::foo()";
    }
};

class Derived : public Base{
    public:
    void foo(){
        std::cout << "\n" << "Derived::foo()";
    }
};

int main(){
    Base base_obj;
    Derived derived_obj;
    Base* base_ptr{nullptr};

    base_ptr = &base_obj;
    base_ptr->foo();

    base_ptr = &derived_obj;
    base_ptr->foo();
    return 0;
}

Run at Compiler Explorer

Output:

Base::foo()
Base::foo()

The version of foo invoked is determined at compile time, based on the pointer type.

If we declare the foo() as a virtual method, then the version of foo() invoked is resolved dynamically on-the-fly depending on the type of object being pointed to (pointee).

#include <iostream>

class Base
{
    public:
    virtual void foo(){
        std::cout << "\n" << "Base::foo()";
    }
};

class Derived : public Base{
    public:
    void foo() override{    //override is optional
        std::cout << "\n" << "Derived::foo()";
    }
};

int main(){
    Base b;
    Derived d;
    
    // Virtual function call through reference
    Base& br = b;   // the type of br is Base&
    Base& dr = d;   // the type of dr is Base&
    br.foo();       // Calls Base::foo()
    dr.foo();       // Calls Derived::foo()

    // Virtual function call through pointers
    Base* bp = &b;  // type of bp is Base*
    Base* dp = &d;  // type of dp is Base* as well
    bp->foo();      // Calls Base::foo()
    dp->foo();      // Calls Derived::foo()

    // Non-virtual function calls
    br.Base::foo();
    dr.Base::foo();
    return 0;
}

Run at Compiler Explorer

Output:

Base::foo()
Derived::foo()
Base::foo()
Derived::foo()
Base::foo()
Base::foo()

A derived class virtual function is considered an override if and only if it has the same

  • name
  • parameter type list (but not the return type)
  • cv-qualifiers
  • ref-qualifiers
#include <iostream>

class Base
{
    public:
    virtual void vf(){
        std::cout << "\n" << "Base::vf()";
    }
};

class Derived : public Base{
    public:
    void vf() const{
        std::cout << "\n" << "Derived::vf() const";
    }
};

int main(){
    Base b;
    Derived d;
    const Derived const_d;

    Base* bp{nullptr};

    bp = &b;
    bp->vf();

    bp = &d;
    bp->vf();
    d.vf();
    return 0;
}

Output:

Base::vf()
Base::vf()
Derived::vf() const

In the above code snippet, the compiler does not treat void Derived::vf() const as an override for the base class virtual member function void Base::vf().

Dynamic dispatch

A member function defined as virtual in the base class will be virtual in all child classes.

#include<iostream>
struct A{
    virtual void vf(){
        std::cout << "\n" << "A::vf()";
    }

    void g(){
        std::cout << "\n" << "A::g()";
    }
};
 
struct B : A{
    void vf() override{
        std::cout << "\n" << "B::vf()";
    }

    void g(){
        std::cout << "\n" << "B::g()";
    }
};

struct C : B{
    void vf() override{
        std::cout << "\n" << "C::vf()";
    }

    void g(){
        std::cout << "\n" << "C::g()";
    }
};

struct D : C{
    void g(){
        std::cout << "\n" << "D::g()";
    }
};

int main()
{
    A a; B b; C c; D d;
    A* a_as_aptr = &a;
    A* b_as_aptr = &b;
    A* c_as_aptr = &c;
    A* d_as_aptr = &d;

    a_as_aptr->vf();
    b_as_aptr->vf();
    c_as_aptr->vf();
    d_as_aptr->vf();

    a_as_aptr->g();
    b_as_aptr->g();
    c_as_aptr->g();
    d_as_aptr->g();
}

Run at Compiler Explorer

Output:

A::vf()
B::vf()
C::vf()
C::vf()
A::g()
A::g()
A::g()
A::g()

Because vf() is a virtual function, the dynamic type (type of the pointee object) is used at run-time to resolve calls to b_as_aptr->vf() or c_as_aptr->vf(). With non-virtual functions such as g(), the compiler uses the static type to determine what function to call, and it can do so at compile-time.

override: a useful feature to prevent bugs

Every time you define a method in the derived class that override virtual member function in the base class, as a good practice, tag it override. This way, you show that your intention for the derived class is to override the behavior of vf in the base class.

#include<iostream>
struct Base{
    virtual void vf(){
        std::cout << "\n" << "void Base::vf()";
    }
};

struct Derived : public Base{
    void vf() override{
        std::cout << "\n" << "void Derived::vf()";
    }
};

If a function is declared with specifier override but does not override a base class virtual member function, the program is ill-formed and will not compile.

#include<iostream>
struct Base{
    virtual void vf(){
        std::cout << "\n" << "void Base::vf()";
    }
};

struct Derived : public Base{
    void vf(long) override{
        std::cout << "\n" << "void Derived::vf()";
    }
};

Run at Compiler Explorer

Base::vf() does not need to be accessible or visible to be overriden. Base::vf() can be declared as private, or Base can be inherited using private inheritance.

#include<iostream>
class B
{
    virtual void do_f(){ // private member
        std::cout << "\n" << "B::do_f()";
    }
    public:
    void f() { do_f(); } // public interface
};
 
class D : public B
{
    void do_f() override{ // overrides B::do_f
        std::cout << "\n" << "D::do_f()";
    }
};
 
int main()
{
    D d;
    B* bp = &d;
    bp->f(); // internally calls D::do_f();
}

Run at Compiler Explorer

virtual destructors

Consider the following code snippet, where we intend to use a class hierarchy polymorphically:

#include<iostream>
struct A{
    A(){ std::cout << "\n" << "A()"; }
    ~A(){ std::cout << "\n" << "~A()"; }
};

struct B : A{
    B(){ std::cout << "\n" << "B()"; }
    ~B(){ std::cout << "\n" << "~B()"; }
};

struct C : B{
    C(){ std::cout << "\n" << "C()"; }
    ~C(){ std::cout << "\n" << "~C()"; }
};

int main(){
    A* c_as_aptr{new C()};
    delete c_as_aptr;
    return 0;
}

Run at Compiler Explorer

Output:

A()
B()
C()
~A()

new C() dynamically allocates memory on the heap and default-initializes a C() object in this memory block. The address of the newly constructed object is stored in c_as_aptr pointer variable.

If we comment out line 19, we shall leak memory. All calls to new or new[] must be matched by corresponding calls to delete or delete[].

delete T for a type T calls the destructor ~T() and deallocates the memory space occupied by T.

Since the base-class destructor ~A() is a non-virtual function, delete c_as_aptr has undefined behavior. In most implementations, the call to the destructor will be resolved like any non-virtual code, meaning that the destructor of the base class will be called but not the one of the derived class, resulting in a resources leak.

Always make base classes’ destructors virtual when they’re meant to be manipulated polymorphically.

#include<iostream>
struct A{
    A(){ std::cout << "\n" << "A()"; }
    virtual ~A(){ std::cout << "\n" << "~A()"; }
};

struct B : A{
    B(){ std::cout << "\n" << "B()"; }
    ~B(){ std::cout << "\n" << "~B()"; }
};

struct C : B{
    C(){ std::cout << "\n" << "C()"; }
    ~C(){ std::cout << "\n" << "~C()"; }
};

int main(){
    A* c_as_aptr{new C()};
    delete c_as_aptr;
    return 0;
}

Run at Compiler Explorer

Output:

A()
B()
C()
~C()
~B()
~A()

virtual inheritance and the diamond problem

Unlike non-virtual inheritance described above, virtual inheritance uses the keyword virtual as the access specifier, when inheriting from a base class.

The difference between conventional inheritance and virtual inheritance is that virtual inheritance allows a diamond class hierarchy and child classes inherit only one copy of the virtual base class.

Consider the following code snippet:

struct A{};
struct B : A {};
struct C : A {};
struct D : B, C {};

Here, D inherits indirectly from A, it inherits a copy of class A from via B and a second copy of class A via C. So, D inherits two copies of class A.

A   A
|   |
B   C
 \ /
  D
#include<iostream>
struct A{
    A(){ std::cout << "\n" << "A()";}
    virtual ~A(){ std::cout << "\n" << "~A()"; }
};

struct B : A{
    B(){ std::cout << "\n" << "B()";}
    virtual ~B(){ std::cout << "\n" << "~B()"; }
};

struct C : A{
    C(){ std::cout << "\n" << "C()";}
    virtual ~C(){ std::cout << "\n" << "~C()"; }
};

struct D : B, C{
    D(){ std::cout << "\n" << "D()";}
    virtual ~D(){ std::cout << "\n" << "~D()"; }
};

int main(){
    D d;
    return 0;
}

Run at Compiler Explorer

Output:

A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()

To solve this problem, we must make A as a virtual base class for both B and C. D inherits from both B and C. This time, because A is inherited virtually by B and C only one instance of the A subobject will be created for `D.

#include<iostream>
struct A{
    A(){ std::cout << "\n" << "A()";}
    virtual ~A(){ std::cout << "\n" << "~A()"; }
};

struct B : virtual public A{
    B(){ std::cout << "\n" << "B()";}
    virtual ~B(){ std::cout << "\n" << "~B()"; }
};

struct C : virtual public A{
    C(){ std::cout << "\n" << "C()";}
    virtual ~C(){ std::cout << "\n" << "~C()"; }
};

struct D : public B, public C{
    D(){ std::cout << "\n" << "D()";}
    virtual ~D(){ std::cout << "\n" << "~D()"; }
};

int main(){
    D d;
    return 0;
}

Run at Compiler Explorer

Output:

A()
B()
C()
D()
~D()
~C()
~B()
~A()

Class Hierarchy, Virtual Tables, Virtual Table Table

Consider the following class hierarchy:

struct A{
    A() = default;
    virtual void foo() = 0;
    virtual void baz() {};
    virtual ~A() = default;
};

struct B{
    B() = default;
    virtual void foo(){};
    virtual void baz(){};
    virtual ~B() = default;
};

struct C : B{
    C() = default;
    void foo() override {}
    void c_bar() const{}
    virtual void bar() {}
    virtual ~C() = default;
};

struct D : C, A{
    D() = default;
    // Overriding A virtual functions
    virtual void foo() override {}
    
    // Overriding C virtual functions
    virtual void bar()override {}

    // non-virtual functions
    void d_bar() {}
    virtual ~D() = default;
};

int main(){
    D d;
    return 0;
}

We could dump the class hierarchy information including the virtual table information, using the gcc compiler flag -fdump-lang-class.

[quantdev@quasar-arch virtual_tables]$ g++ -g -fdump-lang-class main.cpp -o main
[quantdev@quasar-arch virtual_tables]$ cat main.cpp.001l.class
Vtable for A
A::_ZTV1A: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))__cxa_pure_virtual
24    (int (*)(...))A::baz
32    0
40    0

Class A
   size=8 align=8
   base size=8 base align=8
A (0x0x7cb4a0bd8000) 0 nearly-empty
    vptr=((& A::_ZTV1A) + 16)

Vtable for B
B::_ZTV1B: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1B)
16    (int (*)(...))B::foo
24    (int (*)(...))B::baz
32    (int (*)(...))B::~B
40    (int (*)(...))B::~B

Class B
   size=8 align=8
   base size=8 base align=8
B (0x0x7cb4a0bd8120) 0 nearly-empty
    vptr=((& B::_ZTV1B) + 16)

Vtable for C
C::_ZTV1C: 7 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1C)
16    (int (*)(...))C::foo
24    (int (*)(...))B::baz
32    (int (*)(...))C::~C
40    (int (*)(...))C::~C
48    (int (*)(...))C::bar

Class C
   size=8 align=8
   base size=8 base align=8
C (0x0x7cb4a0a0e618) 0 nearly-empty
    vptr=((& C::_ZTV1C) + 16)
B (0x0x7cb4a0bd8180) 0 nearly-empty
      primary-for C (0x0x7cb4a0a0e618)

Vtable for D
D::_ZTV1D: 13 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1D)
16    (int (*)(...))D::foo
24    (int (*)(...))B::baz
32    (int (*)(...))D::~D
40    (int (*)(...))D::~D
48    (int (*)(...))D::bar
56    (int (*)(...))-8
64    (int (*)(...))(& _ZTI1D)
72    (int (*)(...))D::_ZThn8_N1D3fooEv
80    (int (*)(...))A::baz
88    (int (*)(...))D::_ZThn8_N1DD1Ev
96    (int (*)(...))D::_ZThn8_N1DD0Ev

Class D
   size=16 align=8
   base size=16 base align=8
D (0x0x7cb4a0be7000) 0
    vptr=((& D::_ZTV1D) + 16)
C (0x0x7cb4a0a0e8f0) 0 nearly-empty
      primary-for D (0x0x7cb4a0be7000)
B (0x0x7cb4a0bd8240) 0 nearly-empty
        primary-for C (0x0x7cb4a0a0e8f0)
A (0x0x7cb4a0bd82a0) 8 nearly-empty
      vptr=((& D::_ZTV1D) + 72)

vtable for class A

A::_ZTV1A: 6 entries says the name of the vtable, which is a static array, is A::_ZTV1A and there are \(6\) function pointer entries in this static array. The offset of each entry is \(8\) bytes.

Vtable for A
A::_ZTV1A: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))__cxa_pure_virtual
24    (int (*)(...))A::baz
32    0
40    0

Class A
   size=8 align=8
   base size=8 base align=8
A (0x0x7cb4a0bd8000) 0 nearly-empty
    vptr=((& A::_ZTV1A) + 16)

16 (int (*)(...))__cxa_pure_virtual and 24 (int (*)(...))A::baz are pointers to the pure virtual function A::foo()=0 and the virtual function A::baz() respectively.

vtable for class B

Vtable for B
B::_ZTV1B: 6 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1B)
16    (int (*)(...))B::foo
24    (int (*)(...))B::baz
32    (int (*)(...))B::~B
40    (int (*)(...))B::~B

Class B
   size=8 align=8
   base size=8 base align=8
B (0x0x7cb4a0bd8120) 0 nearly-empty
    vptr=((& B::_ZTV1B) + 16)

Again, 16 (int (*)(...))B::foo and 24 (int (*)(...))B::baz are pointers to the virtual functions B::foo() and B::baz().

The entries for virtual destructors are actually pairs of entries.

The first destructor, 32 (int (*)(...))B::~B called the complete object destructor, only performs destruction of variables that live on the stack having automatic storage duration. This memory does not need to be deallocated.

The second destructor, 40 (int (*)(...))B::~B called the deleting destructor of T is function, that in addition, to calling the complete object destructor, also calls the appropriate deallocation function for T (operator delete on T).

Since the class A has atleast one pure virtual method, it cannot be instantiated directly, hence its vtable does not contain entries for destructors.

vtable for class C

Vtable for C
C::_ZTV1C: 7 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1C)
16    (int (*)(...))C::foo
24    (int (*)(...))B::baz
32    (int (*)(...))C::~C
40    (int (*)(...))C::~C
48    (int (*)(...))C::bar

Class C
   size=8 align=8
   base size=8 base align=8
C (0x0x7cb4a0a0e618) 0 nearly-empty
    vptr=((& C::_ZTV1C) + 16)
B (0x0x7cb4a0bd8180) 0 nearly-empty
      primary-for C (0x0x7cb4a0a0e618)

C is a child class of B. 16 (int (*)(...))C::foo is a pointer to the overriding function of B::foo() - C::foo() and 24 (int (*)(...))B::baz is a pointer to the inherited function B::baz().

32 (int (*)(...))C::~C and 40 (int (*)(...))C::~C are the pairs of destrutors.

48 (int (*)(...))C::bar is the pointer to the subclass method C::bar().

vtable for class D

D inherits from both A and C.

Vtable for D
D::_ZTV1D: 13 entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1D)
16    (int (*)(...))D::foo
24    (int (*)(...))B::baz
32    (int (*)(...))D::~D
40    (int (*)(...))D::~D
48    (int (*)(...))D::bar
56    (int (*)(...))-8
64    (int (*)(...))(& _ZTI1D)
72    (int (*)(...))D::_ZThn8_N1D3fooEv
80    (int (*)(...))A::baz
88    (int (*)(...))D::_ZThn8_N1DD1Ev
96    (int (*)(...))D::_ZThn8_N1DD0Ev

Class D
   size=16 align=8
   base size=16 base align=8
D (0x0x7cb4a0be7000) 0
    vptr=((& D::_ZTV1D) + 16)
C (0x0x7cb4a0a0e8f0) 0 nearly-empty
      primary-for D (0x0x7cb4a0be7000)
B (0x0x7cb4a0bd8240) 0 nearly-empty
        primary-for C (0x0x7cb4a0a0e8f0)
A (0x0x7cb4a0bd82a0) 8 nearly-empty
      vptr=((& D::_ZTV1D) + 72)

16 (int (*)(...))D::foo is a pointer to the overriding function of A:foo() and C::foo() - D::foo().

24 (int (*)(...))B::baz and 80 (int (*)(...))A::baz are pointers to the copies of baz() inherited through the parents A and B.

32 (int (*)(...))D::~D and 40 (int (*)(...))D::~D are pairs of destructors.

48 (int (*)(...))D::bar is a pointer to the overriding function of C::bar() - D::bar().

When a class inherits from multiple base classes ( as in the case of class D inheriting from both A and C), the memory layout of the derived class D includes subobjects for each of the base class. The this pointer must be adjusted to point to the correct subobject when calling a virtual function from one of the base classes.

The vcall-offset is an adjustment value that ensures that the this pointer points to the correct base class subobject, when a virtual function is invoked.

References