Virtual Functions, vtable and Object Slicing in C++

Difficulty: Advanced

Question

What are virtual functions in C++? How is dynamic dispatch implemented with a vtable, why do we need virtual destructors, and what is object slicing?

Answer

This is where C++ and Java diverge most, and if your interview mentions C++ on the resume, expect this question. It is also great for showing that you understand what the runtime does under the hood.

In C++, dynamic binding is opt-in. A member function declared virtual in the base class is dispatched according to the actual object type when called through a pointer or reference. Without virtual, the call is resolved statically using the pointer's declared type. In the example below, both hello and plain are called through a Base pointer holding a Derived object. hello is virtual, so it prints Derived. plain is not, so it prints Base plain even though the object is a Derived. Java made every instance method virtual by default, C++ chose zero cost unless you ask for it.

How is it implemented? For every class with virtual functions, the compiler creates a virtual table, the vtable: an array of function pointers, one per virtual function, with derived classes overriding the entries they redefine. Each object of such a class carries a hidden pointer, the vptr, set by the constructor to point at the vtable of its class. A virtual call becomes: follow the vptr, index the vtable, call through the pointer. That costs one extra indirection and a slightly larger object, but no more. A class with a pure virtual function (virtual void f() = 0) is abstract and cannot be instantiated. Using the override specifier is best practice since the compiler then checks that you really override something.

Now the virtual destructor, which is a classic interview point. If you delete a Derived object through a Base pointer and the base destructor is not virtual, only the Base destructor runs; the derived part is never cleaned up. This is undefined behaviour and typically leaks resources. The rule: any class intended to be used polymorphically should have a virtual destructor. In the code, with a virtual ~Base, delete p prints the derived destructor first and then the base one.

Object slicing is the last trap. If you copy a derived object into a base object by value, as in Base b = d, only the Base portion is copied. The derived members and the vptr of the new object are those of Base, so b.hello() prints Base, even though hello is virtual. Polymorphism only works through pointers or references. This is why polymorphic classes are passed by reference or by smart pointer, and why containers like vector of Base cannot hold Derived objects polymorphically; use vector of unique_ptr of Base instead.

Additional interview points. Constructors cannot be virtual because the vptr is not set until construction. Calling virtual functions from constructors or destructors resolves to the version of the class currently being constructed, not the most derived class. Static functions cannot be virtual. And the compiler can devirtualise a call when it knows the exact type.

The contrast with Java: every non-static, non-private, non-final method is virtual; there is no slicing since objects are only handled through references; and cleanup is by garbage collection and try-with-resources, so virtual destructors are not a concern.

Code examples

Virtual dispatch, virtual destructor, and slicing

#include <iostream>
using namespace std;

struct Base {
    virtual void hello() { cout << "Base\n"; }
    void plain()         { cout << "Base plain\n"; }
    virtual ~Base()      { cout << "~Base\n"; }
};

struct Derived : Base {
    void hello() override { cout << "Derived\n"; }
    void plain()          { cout << "Derived plain\n"; }
    ~Derived()            { cout << "~Derived\n"; }
};

int main() {
    Base* p = new Derived();
    p->hello();     // virtual: runtime type
    p->plain();     // non-virtual: pointer type
    delete p;       // virtual destructor: Derived then Base

    Derived d;
    Base b = d;     // slicing: only the Base part is copied
    b.hello();
}

After b.hello(), main ends: b is destroyed first (~Base), then d (~Derived, ~Base).

Key points

Concepts covered

virtual-function, vtable, virtual-destructor, object-slicing, C++