Virtual Function and Pure Virtual Function in C++
In object‑oriented programming, polymorphism lets a single interface represent different underlying forms. C++ achieves runtime polymorphism through virtual functions and their special case, pure virtual functions. Understanding these mechanisms is essential for designing flexible class hierarchies, building abstract bases, and writing code that behaves correctly when objects are accessed via pointers or references to base classes But it adds up..
What Is a Virtual Function?
A virtual function is a member function declared in a base class with the virtual keyword. When a derived class overrides this function, the version that gets executed is determined at runtime based on the actual object type, not the pointer or reference type used to call it. This mechanism is called dynamic binding or late binding And it works..
class Shape {
public:
virtual void draw() const { // virtual function
std::cout << "Drawing a generic shape\n";
}
virtual ~Shape() = default; // virtual destructor (good practice)
};
class Circle : public Shape {
public:
void draw() const override { // override specifier (C++11+)
std::cout << "Drawing a circle\n";
}
};
If we store a Circle object in a Shape* pointer and invoke draw(), the call resolves to Circle::draw():
Shape* s = new Circle;
s->draw(); // Output: Drawing a circle
delete s;
Without virtual, the call would bind to Shape::draw() at compile time, producing static binding and losing polymorphic behavior.
How Virtual Functions Work Internally
Compilers typically implement virtual functions using a virtual table (vtable) and a vpointer hidden inside each object:
- Vtable – an array of function pointers, one per virtual function, created per class that declares or overrides virtual functions.
- Vpointer – a hidden pointer stored in each object instance, pointing to the vtable of its most‑derived class.
When a virtual function is invoked through a base pointer/reference, the CPU fetches the vpointer, indexes into the vtable, and jumps to the address stored there. This indirection adds a tiny overhead but enables the correct overridden function to be selected dynamically.
Pure Virtual Functions and Abstract Classes
A pure virtual function is declared by assigning 0 in its declaration:
class Shape {
public:
virtual void draw() const = 0; // pure virtual function
virtual ~Shape() = default;
};
A class containing at least one pure virtual function becomes an abstract class. Abstract classes cannot be instantiated directly; they serve only as bases for concrete derived classes that provide implementations for all pure virtual members.
class Circle : public Shape {
public:
void draw() const override { // must provide definition
std::cout << "Drawing a circle\n";
}
};
Attempting to create a Shape object results in a compile‑time error:
Shape s; // Error: cannot declare variable 's' to be of abstract type 'Shape'
Pure virtual functions define a contract: any concrete subclass must fulfill it. This is the C++ way to express interfaces, similar to Java’s interface or C#’s abstract members.
The override and final Specifiers
Starting with C++11, the contextual keyword override helps catch mistakes when intending to override a base virtual function. If the signature does not match any virtual function in a base class, the compiler issues an error The details matter here. Simple as that..
class Derived : public Base {
public:
void foo(int) override; // Error if Base::foo() takes no parameters or is not virtual
};
The final specifier prevents further overriding in derived classes:
class Base {
public:
virtual void bar() final; // Cannot be overridden
};
Rules and Best Practices
| Rule | Explanation |
|---|---|
| Declare destructors virtual in polymorphic base classes | Ensures proper cleanup when deleting a derived object via a base pointer. |
| Keep virtual functions minimal | Each virtual function adds a vtable entry; excessive virtuality can impact performance. |
Prefer override |
Makes intent explicit and helps the compiler detect mismatches. |
| Avoid calling virtual functions from constructors/destructors | The dynamic type is not yet fully formed; the call resolves to the constructor’s own class version. |
| Make pure virtual functions public (or protected) as needed | Accessibility follows normal member rules; they can be protected if only derived classes should invoke them directly. |
| Provide a definition for a pure virtual function if needed | A pure virtual can still have a body (e.g.So naturally, , for a common base implementation) but must be declared with = 0. Derived classes must still override it. |
Example: A Simple Shape Hierarchy
Below is a compact, self‑contained example that demonstrates virtual functions, pure virtual functions, abstract bases, and proper use of override and a virtual destructor.
#include
#include
#include
class Shape { // abstract base
public:
virtual void draw() const = 0; // pure virtual -> interface
virtual void rotate(double angle) const { // regular virtual with default behavior
std::cout << "Rotating shape by " << angle << " degrees\n";
}
virtual ~Shape() = default; // virtual destructor
};
class Circle : public Shape {
private:
double radius_;
public:
explicit Circle(double r) : radius_(r) {}
void draw() const override { // override required
std::cout << "Circle radius " << radius_ << " drawn\n";
}
// inherit rotate() from Shape (uses default)
};
class Rectangle : public Shape {
private:
double width_, height_;
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
void draw() const override {
std::cout << "Rectangle " << width_ << "x" << height_ << " drawn\n";
}
void rotate(double angle) const override { // customized behavior
std::cout << "Rectangle rotated " << angle << " degrees (width/height swapped)\n";
}
};
void renderShapes(const std::vector>& shapes) {
for (const auto& s : shapes) {
s->draw();
s->rotate(45.0);
}
}
int main() {
std::vector> gallery;
gallery.push_back(std::make_unique(2.5));
gallery.push_back(std::make_unique(3.0, 4.0));
renderShapes(gallery);
return 0;
}
Output
Circle radius 2.5 drawn
Rotating shape by
Circle radius 2.5 drawn Rotating shape by 45 degrees Rectangle 3x4 drawn Rectangle rotated 45 degrees (width/height swapped)
### Key Takeaways from the Example
1. **Polymorphic Dispatch**: `renderShapes` operates on a collection of `std::unique_ptr`. It knows nothing about `Circle` or `Rectangle` concrete types, yet the correct `draw()` and `rotate()` implementations are invoked at runtime.
2. **Default vs. Overridden Behavior**: `Circle` inherits `Shape::rotate()` (printing a generic message), while `Rectangle` overrides it to provide specialized logic (swapping width/height). This demonstrates how virtual functions allow "pay-for-play" customization.
3. **Resource Safety**: The use of `std::unique_ptr` combined with a `virtual` destructor in `Shape` ensures that when the vector is destroyed (or elements are erased), the derived destructors (`~Circle`, `~Rectangle`) run correctly, preventing resource leaks.
4. **Explicit Contracts**: The `override` specifier on `Circle::draw` and `Rectangle::rotate` guarantees at compile time that these functions actually override a base virtual function, catching signature mismatches (e.g., `const` qualification differences) early.
---
## Conclusion
Virtual functions are the cornerstone of runtime polymorphism in C++, enabling flexible, extensible designs where algorithms operate on abstractions rather than concrete implementations. By mastering the mechanics—`virtual`, `override`, `final`, pure virtual functions (`= 0`), and the critical virtual destructor—you gain the ability to build class hierarchies that are both type-safe and maintainable.
Remember the core rules: **always declare destructors `virtual` in polymorphic bases**, **use `override` religiously** to let the compiler verify your intent, and **avoid virtual calls during construction or destruction** when the dynamic type is incomplete. When applied with discipline, these features transform rigid class structures into powerful frameworks capable of evolving with changing requirements—without modifying the code that depends on the base interface.