C++ Deep Dive: Virtual Functions - CSU1287 - Shoolini U

Virtual Functions in C++

Executive Summary

In the realm of C++ and Object Oriented Programming, virtual functions serve as a cornerstone. At their simplest, they are base class functions that can be redefined in derived classes, enabling runtime polymorphism and abstraction. They provide a base for flexible and reusable code, making them critical for large software systems.

A variant of these are pure virtual functions, which have no definition in the base class, rendering such classes abstract. Another key concept is the virtual destructor, ensuring that correct cleanup of resources is done when objects are destroyed.

Internally, virtual functions are managed using the Virtual Table (Vtable) and Virtual Pointer (Vptr), structures created by the compiler to enable dynamic function calls. While these can add to memory and performance overheads, the benefits of virtual functions typically outweigh these costs.

With this understanding of virtual functions, one can effectively navigate the complexities of runtime polymorphism and abstraction, key to C++ Object Oriented Programming.

1. Introduction to Virtual Functions

Imagine you're playing with a collection of different shaped blocks, each with a specific action associated with it. When you push a button, each block performs its unique action. The problem arises when you have to code for each individual shape and their respective actions; it becomes repetitive and inefficient. The answer to this problem in C++ Object Oriented Programming (OOP) is using a feature called virtual functions.

At a very simple level, a virtual function is a member function in the base class that you redefine in a derived class. It is declared using the keyword "virtual" in the base class, and it is used to tell the compiler to perform 'Late Binding' or 'Dynamic Method Dispatch'. This means that the function to be invoked will be determined at runtime, rather than at compile time, thereby providing the flexibility we require for our scenario.

To understand this, imagine our different shaped blocks are different classes and the actions are their functions. If we have a base class 'Block', and derived classes 'Circle', 'Square' and 'Triangle', each with a function 'action', instead of defining what the 'action' does for every shape, we just call the 'action' function on the 'Block' and let it decide which specific function to execute. This is the power of a virtual function.

Now, let's dive deeper into the intricacies of this powerful feature of OOP.

2. Details of Virtual Functions

In a programming language like C++, where we use the concepts of classes and objects, the virtual functions play a very important role. The fundamental idea is that we can have a function in a base class that can be overridden in a derived class. This essentially allows us to use polymorphism, which is a pillar of object-oriented programming.

In essence, a virtual function is a function in a base class that is declared using the keyword virtual. Defining in a base class a virtual function, with another version in a derived class, signals to the compiler that we don't want static linkage for this function. Instead, we want dynamic linkage, i.e., the function call is resolved at runtime.

For example, consider a base class called 'Shape' with a virtual function 'draw()'. If we have two derived classes, 'Circle' and 'Square', both with their versions of 'draw()', depending on the type of object pointer that we use to call 'draw()', the appropriate version will be called. If a 'Circle' object pointer is used, 'Circle's draw()' is called, and if a 'Square' object pointer is used, 'Square's draw()' is called.


class Shape {
public:
    virtual void draw() {
        cout << "Drawing a generic shape." << endl;
    }
};

class Circle : public Shape {
public:
    void draw() {
        cout << "Drawing a circle." << endl;
    }
};

class Square : public Shape {
public:
    void draw() {
        cout << "Drawing a square." << endl;
    }
};

This property is really powerful and forms the foundation of runtime polymorphism, which allows us to write more flexible and reusable code.

Demo Program for Virtual Functions
#include <iostream>

class Base {
public:
    virtual void print() { 
        std::cout << "This is Base class.\n"; 
    }
};

class Derived : public Base {
public:
    void print() override { 
        std::cout << "This is Derived class.\n"; 
    }
};

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

    return 0;
}

In this program, we declare a base class named 'Base' with a virtual function named 'print()'. Then, we declare a derived class named 'Derived' that overrides the 'print()' function. In the 'main()' function, we create an object of the derived class, assign the address of the derived class object to the base class pointer, and then call the 'print()' function using the base class pointer. Since 'print()' is a virtual function, the derived class's version of 'print()' will be called, outputting "This is Derived class." to the console.

2.1 Pure Virtual Functions

There is a special category of virtual functions known as pure virtual functions. A pure virtual function is a function that has no definition in the base class. Instead, the base class only provides a function declaration. The pure virtual function must be defined in any non-abstract class that directly or indirectly inherits from the class that declares the function.

Declaring a pure virtual function is done by using the 'virtual' keyword and setting the function's value equal to 0 in the base class.


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

A class containing pure virtual function(s) is known as an abstract class. Objects of such a class cannot be created. Instead, the abstract class acts as a base class for other classes.

This forms the basis for a powerful feature of OOP called abstraction, which allows us to write code that specifies what to do, but not how to do it.

2.2 Virtual Destructor

Along with virtual functions, there's a concept of a virtual destructor. Like a regular destructor, a virtual destructor is used to clean up any resources allocated by the object before it is destroyed. However, a virtual destructor adheres to the rules of a virtual function and ensures that the correct destructor is called for an object, particularly when we deal with base class pointers to derived class objects.


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

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived Destructor" << endl;
    }
};

int main() {
    Base *ptr = new Derived();
    delete ptr; // Calls the destructor for Derived, then Base
    return 0;
}

In this code, if the Base destructor was not declared virtual, only the Base destructor would be called, leading to a potential resource leak. Thus, it's a good practice to always make base class destructors virtual when dealing with inheritance.

2.3 The Virtual Table and Vptr

So how does C++ handle these virtual functions under the hood? It's actually quite clever, and it involves a couple of key structures: the Virtual Table and Vptr.

When a class contains a virtual function, the compiler internally creates a table called the Vtable (Virtual Table). Each entry in the Vtable is a function pointer that points to the virtual function in the class. The compiler also adds a hidden pointer in every object of the class, called Vptr (Virtual Pointer), which points to the Vtable of that class. This is how the correct function is determined at runtime.

When a derived class overrides a virtual function, the compiler replaces the function pointer in the Vtable with the address of the derived class's function. If the derived class does not provide a new definition, the Vtable keeps the base class function's address.

Although these are compiler specifics and can vary between different compilers, knowing about them provides a better understanding of how virtual functions work internally, which could be crucial for high-performance or low-level code.

3. Use Cases and Benefits of Virtual Functions

Virtual functions are a powerful feature of OOP in C++. They allow us to achieve polymorphism, one of the main tenets of OOP, which makes our code flexible and reusable. They are used when we have a base class pointer to a derived class object, and we want to invoke derived class functions using this base class pointer.

Virtual functions are particularly useful in large software systems where a base class is used to define a general interface that is common for all derived classes. By making the interface functions virtual, each derived class can provide its specific implementation, while the client code can remain generic as it only interacts with the base class.

Moreover, virtual functions provide a template for other functions in derived classes. They lead to cleaner code by avoiding redundant code and reducing complexity, enhancing maintainability.

However, like any other feature, virtual functions also have their costs. They can have a performance impact due to the extra level of indirection during runtime function resolution, and they use additional memory for the Vtable and Vptr. Therefore, they should be used judiciously where the benefits of flexibility, maintainability, and code reuse outweigh the costs.

4. Conclusion

Thus, we come to the end of our enlightening journey through the realm of virtual functions, a kingdom that lies at the heart of C++ and Object Oriented Programming. These functions, like benevolent guides, unlock the power of runtime polymorphism and abstraction, leading us to vistas of code that are flexible, reusable, and profoundly efficient.

As we've seen, virtual functions might seem like complex creatures at first, bearing the weight of an extra layer of abstraction and performance overhead. Yet, like seasoned explorers, we know that treasures often lie beyond challenges. The gems of code reuse, maintainability, and the ability to work with generalized interfaces are indeed priceless, typically overshadowing the costs.

As we conclude this chapter, consider the virtual function as an indispensable companion in your C++ odyssey, a tool that needs to be understood in all its intricate details to truly harness its potential. A tool that, in the right hands, can mold code into art.

But our journey does not end here. In our next chapter, we'll sail further into the vast seas of C++ Object Oriented Programming. Awaiting us is the mystical land of multiple inheritance and its intricate dance with virtual functions, leading to more complex and fascinating class hierarchies. So, fellow adventurers, tighten your belts and get ready for another exciting dive into the depths of C++!