C++ Virtual Functions

Virtual functions in C++ are a key concept in object-oriented programming. They enable polymorphism, allowing objects of different classes to be treated as objects of a common base class. They allow derived classes to provide a different implementation of a function declared in the base class. In other words, virtual functions provide a mechanism for late binding, which is the process of linking a function call to the actual function to be executed at run time.

The "virtual" keyword is used to declare a virtual function in the base class. When a virtual function is called through a reference or pointer to the base class, the actual function called will be the one defined in the derived class. This is known as dynamic dispatch or runtime polymorphism.

What is Polymorphism ?

Polymorphism, a core principle of OOP, allows objects of different types to be treated as objects of a common base type. It enables a single interface to represent various types and is achieved through mechanisms such as function overloading and virtual functions.

  • Compile-time polymorphism is achieved through function overloading and operator overloading. The decision about which function to call is made at compile time.
  • Run-time polymorphism is achieved through virtual functions. The decision about which function to call is made at runtime. Enables the implementation of the "is-a" relationship between classes.


Introduction to Virtual Functions

  • What are Virtual Functions ? : Virtual functions are functions in a base class that are marked with the virtual keyword. They provide a way to achieve dynamic binding, allowing the correct function to be called at runtime based on the actual type of the object.

  • Syntax of Virtual Functions :
    class Base {
    public:
        virtual void virtualFunction() {
            // Base class implementation
        }
    };
    
    In this example, virtualFunction is a virtual function in the Base class.

  • Dynamic Binding : Dynamic binding (or late binding) is the process where the actual function call is determined at runtime. It is a key feature of virtual functions, enabling polymorphism.
    Base* obj = new Derived();
    // Calls the overridden function in the Derived class
    obj->virtualFunction(); 
    
    In this example, even though obj is declared as a pointer to the base class (Base), the overridden function in the derived class (Derived) is called at runtime.


Creating a Hierarchy with Virtual Functions

  • Base Class with Virtual Function : Consider a base class Shape with a virtual function draw.
    #include <iostream>
    using namespace std;
    
    class Shape {
    public:
        virtual void draw() {
            cout << "Drawing a shape." << endl;
        }
    };
    

  • Derived Classes with Overridden Functions : Now, create two derived classes, Circle and Rectangle, each overriding the draw function.
    class Circle : public Shape {
    public:
        void draw() override {
            cout << "Drawing a circle." << endl;
        }
    };
    
    class Rectangle : public Shape {
    public:
        void draw() override {
            cout << "Drawing a rectangle." << endl;
        }
    };
    

  • Using Polymorphism : Now, you can use polymorphism to treat objects of different derived classes as objects of the base class.
    int main() {
        Shape* shape1 = new Circle();
        Shape* shape2 = new Rectangle();
        
        // Calls the draw function specific to Circle
        shape1->draw(); 
        // Calls the draw function specific to Rectangle
        shape2->draw(); 
    
        delete shape1;
        delete shape2;
    
        return 0;
    }
    
    In this example, shape1 and shape2 are treated as objects of the base class Shape, but the correct draw function is called based on their actual types at runtime.

C++ Program to Demonstrate the Use of Virtual Functions

#include <iostream>
using namespace std;

class Shape {
public:
  virtual void draw() { cout << "Drawing a 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; }
};

int main() {
  Shape *shape;
  Circle circle;
  Square square;

  shape = &circle;
  shape->draw();

  shape = &square;
  shape->draw();

  return 0;
}

Output
Drawing a Circle
Drawing a Square

In this program, we have created a base class Shape with a virtual function draw(). The draw() function simply outputs a message "Drawing a Shape". We have then created two derived classes Circle and Square, each with its own implementation of the draw() function.

In the main function, we have created pointers to Shape objects and assigned them to instances of the derived classes. When we call the draw() function through the base class pointer, it calls the implementation of the draw() function in the derived class. This demonstrates the use of virtual functions to dynamically dispatch a function call at run time.


Virtual Destructor in C++

  • mportance of Virtual Destructors : When dealing with polymorphism and dynamic memory allocation, it's crucial to have a virtual destructor in the base class. This ensures that the proper destructor is called when deleting objects through a pointer to the base class.
    class Base {
    public:
        virtual ~Base() {
            // Virtual destructor
        }
    };
    

  • Example with Polymorphism and Dynamic Memory :
    int main() {
        Base* obj = new Derived();
        // Calls the destructor of Derived
        delete obj; 
    
        return 0;
    }
    
    In this example, if the base class does not have a virtual destructor, only the base class destructor would be called, leading to a potential memory leak. Adding a virtual destructor ensures the correct destructor is invoked.

Best Practices for Virtual Functions

  • Use Virtual Functions Judiciously : Use virtual functions when there is a need for polymorphism and the "is-a" relationship between classes.

  • Avoid Excessive Use : Avoid making all functions virtual, as this can lead to performance overhead and make the code more complex.

  • Provide Default Implementations : When defining virtual functions in base classes, consider providing default implementations. This allows derived classes to override only when necessary.

  • Use Pure Virtual Functions for Interfaces : Use pure virtual functions to define interfaces. This ensures that derived classes must implement these functions.

  • Consider Performance Implications : Virtual functions introduce a level of indirection, which can impact performance. In performance-critical scenarios, consider alternative design choices.

Conclusion

Virtual functions are a cornerstone of object-oriented programming in C++, enabling polymorphism and creating flexible class hierarchies. By allowing objects of different types to be treated as objects of a common base type, virtual functions enhance code modularity and extensibility.

Understanding the syntax, concepts, and best practices associated with virtual functions is essential for writing maintainable and scalable C++ code. As you design class hierarchies and leverage polymorphism, consider the principles outlined in this tutorial to create robust and efficient software systems. With a solid grasp of virtual functions, you can unlock the full potential of object-oriented design in C++.