Table of contents
In object-oriented programming, virtual functions are a fundamental concept in C++. They allow us to achieve runtime polymorphism (Note: it’s ok if you don’t know what that means), allowing the code to be more flexible and easy to expand. But if you’re wondering why you need them and why they’re part of your curriculum in your University, this article will demystify virtual functions and show you their importance in real-life applications. 🚀
Let’s start with what is Polymorphism and Runtime Polymorphism. 🤔
What is Runtime Polymorphism? 🤖
Runtime Polymorphism in C++ refers to the ability of a program to decide at runtime (when the program is actually running) which method or function to call, based on the type of object that is being referred to by a pointer or a reference.
What Happens Without Virtual Functions?
Let’s start with a simple example:
#include <iostream>
using namespace std;
class Base {
public:
void show() {
cout << "Base class function." << endl;
}
};
class Derived : public Base {
public:
void show() {
cout << "Derived class function." << endl;
}
};
int main() {
Base* basePtr;
Derived derivedObj;
basePtr = &derivedObj;
basePtr->show(); // What will this print?
return 0;
}
You might expect this code to call the show()
function of the Derived class since basePtr
points to a Derived object. However, it will always call the show()
function of the Base class. Hence, the output would be:
Base class function
The Real Reason Code Fails Without Virtual Functions
This happens because the function binding is done at compile time by default. C++ decides which show()
function to call based on the type of the pointer, not the object it points to. In our example, basePtr
is a pointer to the Base class, so the compiler connects the call to Base::show()
based on the pointer type.
This is known as Early Binding or Compile Time Binding , and it’s how C++ usually works. Without virtual functions, the program can't tell at runtime that the pointer is pointing to a derived class object.
Why Base Class Pointers Are Important
At this point, you might think, “Why not just make a derived class pointer? 🤔”. Well, it kills the purpose of polymorphism. While this works for one-off cases, it becomes impractical in real-world scenarios where the type of object is determined dynamically at runtime. Here’s a beautiful example (trust me! 😉) from real life:
Imagine a payment processing system where you have a base class PaymentMethod
and derived classes like CreditCard
, PayPal
, and BankTransfer
. A base class pointer or reference allows you to handle all payment methods uniformly: 💳💻🏦
#include <iostream>
#include <vector>
using namespace std;
class PaymentMethod {
public:
virtual void processPayment() {
cout << "Processing generic payment" << endl;
}
};
class CreditCard : public PaymentMethod {
public:
void processPayment() override {
cout << "Processing credit card payment" << endl;
}
};
class PayPal : public PaymentMethod {
public:
void processPayment() override {
cout << "Processing PayPal payment" << endl;
}
};
int main() {
vector<PaymentMethod*> payments;
payments.push_back(new CreditCard());
payments.push_back(new PayPal());
for (auto payment : payments) {
payment->processPayment();
}
// Clean up memory
for (auto payment : payments) {
delete payment;
}
return 0;
}
Output:
Processing credit card payment
Processing PayPal payment
Using base class pointers, you can handle different payment types without worrying about their specific implementations. This means that when writing payment->processPayment()
, you don't need to know the type of payment method being used. This is the main goal of polymorphism, right? This flexibility is crucial in large-scale systems, where new payment methods can be easily added by creating new derived classes.
Such flexibility is not possible without base class pointers and virtual functions.
What Are Virtual Functions?
Virtual functions tell the compiler to delay function binding until runtime. In simpler words: Virtual functions instruct the compiler to wait until the program is running to decide which function to call. This means the exact function to execute is determined when the program is actually running, not when it's being compile
Let’s modify the first example to use a virtual function:
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base class function." << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class function." << endl;
}
};
int main() {
Base* basePtr;
Derived derivedObj;
basePtr = &derivedObj;
basePtr->show(); // Now this prints: Derived class function.
return 0;
}
The keyword virtual
in the base class function ensures that the call to show()
is resolved at runtime. This process is called late binding or dynamic dispatch.
How Do Virtual Functions Work?
Note: It's okay if you don't grasp everything in this section. 😊
Behind the scenes, virtual functions use a mechanism called the vtable (virtual table). Each class with virtual functions has a vtable, which acts like a lookup table for function pointers. At runtime:
When a virtual function is called on an object, the program checks the vtable to find the right function based on the object's type. 🔍
This ensures the correct function is called, even if the pointer type belongs to the base class. ✅
Why Should You Care About Virtual Functions?
Virtual functions make your code more extensible and maintainable. Here are some real-world scenarios where they shine:
Graphics and Game Development: Different game objects (players, enemies, obstacles) can have a common base class with virtual functions for actions like
move()
orrender()
.UI Frameworks: Widgets like buttons, sliders, and text boxes can inherit from a base
Widget
class and override functions likedraw()
oronClick()
.Polymorphic Libraries: Libraries often use virtual functions to allow users to define custom behavior by inheriting from base classes.
Common Pitfalls to Avoid
Performance Overhead: Virtual functions come with a slight runtime cost because of the need to look up the vtable each time a virtual function is called. This overhead can impact performance, especially in performance-critical applications or when virtual functions are called frequently. Therefore, it's important to use virtual functions judiciously and only when necessary to achieve the desired polymorphic behavior.
Object Slicing: This issue occurs when you assign an object of a derived class to a base class object, rather than a pointer or reference. In such cases, the additional attributes and behaviors specific to the derived class are "sliced" away, leaving only the base class portion. As a result, any overridden virtual functions in the derived class will not be called, which can lead to unexpected behavior and bugs.
Destructors: In the context of polymorphism, it is crucial to declare destructors as
virtual
in base classes. If you fail to do so, the destructors of derived classes will not be invoked when an object is deleted through a base class pointer. This can cause resource leaks, as the cleanup code in the derived class destructor will not run, potentially leaving resources like memory or file handles unfreed. Always ensure that base class destructors are virtual to allow proper cleanup of derived class resources.
Key Takeaways 🚀
Virtual functions enable runtime polymorphism through late binding.
Without virtual functions, function calls are resolved based on the pointer type, which can lead to incorrect behavior.
Base class pointers pointing to derived objects are essential for writing flexible, dynamic code.
Use virtual functions wisely to balance flexibility and performance.
Understanding and mastering virtual functions is crucial for writing professional C++ code. So, next time you wonder why you're learning about virtual functions, remember all that you learned today! 💡
Don’t forget to subscribe to the newsletter! 📬