C++ Virtual Functions - In Depth
Virtual Function Mechanics
Virtual functions enable runtime polymorphism. While the C++ standard does not mandate a specific layout, most implementations use:
1. Compile-time: The compiler emits metadata (e.g., a vtable) and arranges for a per-subobject pointer (often called a vptr) in polymorphic types.
2. Runtime: The vptr selects the appropriate function implementation (dynamic dispatch).
Typical vtable contents/associations include:
- Pointers to the class’s virtual functions (possibly with thunks for adjustments)
- A way to reach RTTI (type_info), implementation-dependent
- Offsets/adjusters for multiple/virtual inheritance when needed
#include <iostream>
struct Base {
virtual void func1() { std::cout << "Base::func1\n"; }
virtual void func2() { std::cout << "Base::func2\n"; }
void nonVirtual() { std::cout << "Base::nonVirtual\n"; }
};
struct Derived : Base {
void func1() override { std::cout << "Derived::func1\n"; }
virtual void func3() { std::cout << "Derived::func3\n"; }
};
// Conceptual (implementation-defined) layout:
// Base object: [vptr] → Base vtable { &Base::func1, &Base::func2 }
// Derived object: [vptr] → Derived vtbl { &Derived::func1, &Base::func2, &Derived::func3 }
Pure Virtual Functions & Abstract Classes
Pure virtual functions (declared with = 0
):
- Make a class abstract (it cannot be instantiated).
- Must be overridden in a concrete derived class for that class to be instantiable.
- May still provide a definition in the base (rare but legal).
Abstract classes can also contain data members and implemented methods.
#include <iostream>
struct DatabaseConnection {
virtual void connect() = 0; // Pure virtual
virtual ~DatabaseConnection() = default; // Polymorphic base needs virtual dtor
void setTimeout(int ms) { timeout = ms; }
protected:
int timeout = 0;
};
struct MySQLConnection : DatabaseConnection {
void connect() override {
std::cout << "Connecting to MySQL with timeout " << timeout << "ms\n";
}
};
override & final Keywords
override:
- Marks a function as overriding a virtual function in a base; the compiler diagnoses mismatches.
final:
- Applied to a virtual function: prevents further overriding.
- Applied to a class: prevents further inheritance from that class (but methods in that class may still override its base).
#include <iostream>
struct Base {
virtual void mustOverride() const = 0;
virtual void canOverride() const {}
virtual void cannotOverride() const final {}
};
struct Derived : Base {
void mustOverride() const override {}
void canOverride() const override {}
// void cannotOverride() const {} // Error: 'final' in Base prevents overriding
};
struct FinalClass final : Derived { // OK: class is final → no further derivation allowed
void canOverride() const override {} // OK: overriding inside the final class is allowed
};
// struct BadChild : FinalClass {}; // Error: cannot inherit from a final class
Virtual Destructors
Rules for polymorphic destruction:
1. Declare base destructors virtual when deleting objects via base pointers/references.
2. Derived destructors are implicitly virtual if the base destructor is virtual.
3. Destruction order is derived first, then base.
If the base destructor is non-virtual and you delete via a base pointer to a derived object, behavior is undefined and resources may leak.
#include <iostream>
struct ResourceHolder {
ResourceHolder() { data = new int[100]; }
virtual ~ResourceHolder() { delete[] data; }
private:
int* data{};
};
struct ExtendedHolder : ResourceHolder {
ExtendedHolder() { extra = new double[50]; }
~ExtendedHolder() override { delete[] extra; }
private:
double* extra{};
};
int main() {
ResourceHolder* h = new ExtendedHolder();
delete h; // Calls ~ExtendedHolder() then ~ResourceHolder()
}
Virtual Calls in Constructors/Destructors
During base-class construction and destruction, dynamic dispatch is suppressed: the effective dynamic type is the class currently being constructed/destructed.
- Overridden functions in derived classes are not called from base constructors/destructors.
- Calling a pure virtual function in a constructor/destructor leads to a runtime error ("pure virtual call").
#include <iostream>
struct B {
B() { f(); } // calls B::f, not D::f
virtual ~B() { f(); } // calls B::f, not D::f
virtual void f() { std::cout << "B::f\n"; }
};
struct D : B {
void f() override { std::cout << "D::f\n"; }
};
int main() {
D d; // prints B::f during construction, B::f during destruction
}
Performance Optimization
- Devirtualization: Compilers may resolve some virtual calls at compile time (especially on
final
classes/methods or with whole-program optimization). - Use
final
on classes/methods when further overriding is unnecessary. - Profile-guided and link-time optimizations can improve indirect-call performance.
- Avoid virtual calls in tight, performance-critical loops; consider CRTP/templates for compile-time polymorphism when appropriate.