C++ supports object-oriented programming with inheritance, but inheritance alone doesn’t give you polymorphism. Polymorphism—calling the “right” function based on the object’s actual type at runtime—comes from the virtual keyword.
This post explains what virtual does, why it matters, and how to use it correctly.
What virtual Means
When you mark a member function as virtual in a base class, you’re telling the compiler:
- Calls through a base-class pointer or reference should be dynamically dispatched (resolved at runtime).
- Derived classes can override that function, and the override will be chosen based on the dynamic type of the object.
Without virtual, calls are typically statically bound (resolved at compile time) based on the static type of the expression.
Static vs Dynamic Dispatch (Why You Need virtual)
Consider a base class and a derived class:
#include <iostream>
struct Animal {
void speak() const {
std::cout << "Animal\n";
}
};
struct Dog : Animal {
void speak() const {
std::cout << "Woof\n";
}
};
int main() {
Dog d;
Animal& a = d; // reference to base
a.speak(); // prints "Animal" (static dispatch)
}
Even though a refers to a Dog, the call prints Animal because speak() is not virtual.
Now make it virtual:
#include <iostream>
struct Animal {
virtual void speak() const {
std::cout << "Animal\n";
}
};
struct Dog : Animal {
void speak() const override {
std::cout << "Woof\n";
}
};
int main() {
Dog d;
Animal& a = d;
a.speak(); // prints "Woof" (dynamic dispatch)
}
With virtual, the program chooses the correct implementation at runtime.
Overriding Correctly: Use override
In modern C++, you should almost always pair virtual functions in derived classes with the override specifier:
struct Base {
virtual void f(int) {}
};
struct Derived : Base {
void f(int) override {} // checked by the compiler
};
Why it matters: small mismatches can silently create a new function instead of overriding.
Example bug (missing const):
struct Base {
virtual void g() const {}
};
struct Derived : Base {
void g() {} // does NOT override; it hides Base::g
};
If you wrote void g() override {}, the compiler would error, saving you from a subtle runtime issue.
Virtual Destructors: Essential for Polymorphic Base Classes
If a class is intended to be used polymorphically (i.e., you delete derived objects through base pointers), the base class destructor must be virtual.
Wrong:
#include <iostream>
struct Base {
~Base() { std::cout << "~Base\n"; }
};
struct Derived : Base {
~Derived() { std::cout << "~Derived\n"; }
};
int main() {
Base* p = new Derived();
delete p; // Undefined behavior: ~Derived may not run
}
Correct:
#include <iostream>
struct Base {
virtual ~Base() { std::cout << "~Base\n"; }
};
struct Derived : Base {
~Derived() override { std::cout << "~Derived\n"; }
};
int main() {
Base* p = new Derived();
delete p; // OK: ~Derived then ~Base
}
Rule of thumb: If your class has any virtual functions, give it a virtual destructor (unless you have a deliberate design reason not to).
Pure Virtual Functions and Abstract Classes
A pure virtual function makes a class abstract (cannot be instantiated) and requires derived classes to implement the function (unless they also remain abstract).
struct Shape {
virtual double area() const = 0; // pure virtual
virtual ~Shape() = default;
};
struct Circle : Shape {
double r;
explicit Circle(double r) : r(r) {}
double area() const override {
return 3.14159 * r * r;
}
};
This is a common pattern for defining interfaces in C++.
Note: A pure virtual destructor is allowed, but it still needs a definition:
struct Interface {
virtual ~Interface() = 0;
};
Interface::~Interface() = default;
How Virtual Dispatch Works (Conceptually)
Most C++ implementations use a vtable (virtual function table) and a vptr (a hidden pointer in the object) to support dynamic dispatch.
- The base class defines a table of function pointers.
- Derived classes replace entries for overridden functions.
- When you call a virtual function through a base pointer/reference, the program consults the vtable at runtime.
You don’t code against vtables directly, but understanding the model helps explain:
- Virtual calls are slightly more expensive than non-virtual calls.
- Objects with virtual functions usually have a slightly larger size (because of the vptr).
Common Pitfalls
Here are frequent issues developers hit when using virtual:
1) Calling virtual functions in constructors/destructors
Virtual dispatch does not behave the way many expect during construction/destruction.
In a base constructor, the dynamic type is treated as the base (the derived part isn’t constructed yet), so calling a virtual function will call the base version.
struct Base {
Base() { f(); }
virtual void f() { /* ... */ }
};
struct Derived : Base {
void f() override { /* ... */ }
};
Base() will call Base::f(), not Derived::f().
2) Object slicing
If you copy a derived object into a base object by value, you “slice off” the derived part.
struct Base { virtual void f() {} };
struct Derived : Base { void f() override {} };
Derived d;
Base b = d; // slicing
b.f(); // calls Base::f (Derived part is gone)
Use pointers/references (often smart pointers) for polymorphism.
3) Forgetting override
As shown earlier, missing override can turn a bug into a silent behavior change.
virtual, final, and override Together
Modern C++ gives you tools to make intent explicit:
override: “This is meant to override a base virtual function.”final: “This function (or class) cannot be overridden (or inherited).”
struct Base {
virtual void run() = 0;
virtual ~Base() = default;
};
struct Impl final : Base {
void run() override {
// implementation
}
};
When Not to Use virtual
virtual is great for runtime polymorphism, but it’s not always the best tool:
- If you don’t need runtime dispatch (compile-time polymorphism via templates may be better).
- If the class is not intended to be a base class.
- If performance or memory constraints are extremely tight and you can avoid dynamic dispatch.
That said, premature avoidance of virtual can make designs more complex than necessary.
Summary
The virtual keyword is the foundation of runtime polymorphism in C++:
- Mark base functions
virtualto enable dynamic dispatch. - Use
overridein derived classes to ensure correct overriding. - Use a
virtualdestructor in polymorphic base classes. - Use pure virtual functions (
= 0) to define abstract interfaces.
Used thoughtfully, virtual helps you build extensible, correct class hierarchies that behave properly through base pointers and references.