Virtual Functions

Virtual functions are member functions declared in a base class and intended to be overridden in derived classes. The keyword virtual is used to enable runtime polymorphism, allowing the program to dynamically determine which function implementation to call based on the actual type of the object rather than the static type of the reference or pointer.

When a base class declares a function as virtual, derived classes can override this function to provide their specific implementation. This facilitates dynamic binding or late binding, where the function call is resolved at runtime.

Why Use Virtual Functions?

Virtual functions enable key object-oriented programming features:

  • Polymorphism: Allows objects of different types to be treated uniformly through pointers or references to the base class, with the correct function being called based on the actual object type.
  • Extensibility: Derived classes can provide specialized behavior by overriding base class functions while maintaining a consistent interface.
  • Dynamic Behavior: The exact function called is determined at runtime, allowing for more flexible and dynamic program behavior.

Syntax of Virtual Functions

To declare a virtual function, use the virtual keyword in the base class. It is usually specified in the function’s declaration, but can also be included in the definition.

class Base {
public:
    virtual void show() {
        std::cout << "Base class show function called." << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived class show function called." << std::endl;
    }
};

In this example, the show function in the Base class is declared as virtual, allowing it to be overridden in the Derived class.

How Virtual Functions Work

  1. Virtual Table (vtable): When a class has virtual functions, the compiler creates a table called the virtual table (vtable) for that class. The vtable contains pointers to the virtual functions available for objects of that class.
  2. Virtual Pointer (vptr): Each object of a class with virtual functions has a virtual pointer (vptr), which points to the vtable of its class. At runtime, when a virtual function is called, the vtable is used to look up the actual function address.
  3. Dynamic Binding: When calling a virtual function via a base class pointer or reference, the program uses the vptr to access the appropriate function in the vtable, allowing the correct function to be called based on the actual derived type.

Example of Using Virtual Functions

Here is an example demonstrating how virtual functions enable polymorphism:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void display() {
        cout << "Display of Base class" << endl;
    }
    void show() {
        cout << "Show of Base class" << endl;
    }
};

class Derived : public Base {
public:
    void display() override {
        cout << "Display of Derived class" << endl;
    }
    void show() {
        cout << "Show of Derived class" << endl;
    }
};

int main() {
    Base* basePtr;
    Derived derivedObj;
    basePtr = &derivedObj;

    // Virtual function, binded at runtime
    basePtr->display(); // Output: "Display of Derived class"

    // Non-virtual function, binded at compile time
    basePtr->show();    // Output: "Show of Base class"

    return 0;
}

In this example:

  • The display function is virtual, so calling basePtr->display() invokes the Derived class implementation due to dynamic binding.
  • The show function is not virtual, so calling basePtr->show() invokes the Base class implementation due to static binding (compile-time).

Overriding Virtual Functions

To override a virtual function in a derived class, define a function with the same signature as the base class’s virtual function. The override keyword can be used for better readability and to ensure that the function is correctly overriding a virtual function.

class Derived : public Base {
public:
    void display() override { // 'override' ensures this function overrides a base virtual function
        cout << "Display of Derived class" << endl;
    }
};

If the function signature does not match any virtual function in the base class, the compiler will generate an error when using override.

Pure Virtual Functions and Abstract Classes

A pure virtual function is a virtual function with no implementation in the base class, making the class abstract. Such a class cannot be instantiated and serves as a base for derived classes.

Syntax for a pure virtual function:

class Base {
public:
    virtual void display() = 0; // Pure virtual function
};

Any class that derives from Base must provide an implementation for the display function. If it doesn’t, that derived class will also be considered abstract.

Rules and Considerations for Virtual Functions

Virtual Destructors: If a class has virtual functions, its destructor should also be declared as virtual to ensure that derived class destructors are called when deleting an object through a base class pointer.

class Base {
public:
    virtual ~Base() {
        cout << "Base destructor called" << endl;
    }
};

Access Specifiers: Virtual functions can have any access specifier (public, protected, or private). However, they are usually declared as public.

Constructors Cannot Be Virtual: Constructors cannot be declared as virtual in C++, as objects are not fully formed during construction.

Static Member Functions: Virtual functions cannot be static because static functions are not tied to a specific object instance.

Multiple Levels of Inheritance: If a class hierarchy has multiple levels, a derived class can still override a base class’s virtual function.

class GrandChild : public Derived {
public:
    void display() override {
        cout << "Display of GrandChild class" << endl;
    }
};

    Performance Considerations

    While virtual functions provide powerful polymorphism, they come with some overhead:

    • Indirection Overhead: Virtual function calls involve an extra level of indirection via the vtable.
    • Increased Memory Usage: The vtable adds some memory overhead for each class with virtual functions.
    • Ineligible for Inlining: Virtual functions are typically not inlined, even if declared with inline, because their address is determined at runtime.

    Example of Virtual Destructors

    Here is an example illustrating the importance of virtual destructors:

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual ~Base() {
            cout << "Base destructor called" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        ~Derived() {
            cout << "Derived destructor called" << endl;
        }
    };
    
    int main() {
        Base* obj = new Derived();
        delete obj; // Both Derived and Base destructors are called
        return 0;
    }

    In this example, if the destructor in the Base class were not virtual, only the Base destructor would be called, leading to potential resource leaks.