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:
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:
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:
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:
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:
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:
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:
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
.
#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:
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:
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.