| Lesson 10 | C++ Virtual Functions |
| Objective | Discuss guidelines for virtual functions in C++23 along with several rules. |
Virtual functions are the core mechanism behind runtime polymorphism in C++. They let you call behavior through a base-class pointer or reference while still executing the derived type’s override. Used well, virtuals enable extension, plugin-style designs, and reusable abstractions. Used poorly, they create brittle hierarchies, unclear ownership, and surprising construction/destruction bugs.
This lesson focuses on practical C++23 guidance: when virtual functions are the right tool, how to write them so overrides are safe and obvious, and which rules keep polymorphic code maintainable.
Static functions have no dynamic object, so there is nothing to dispatch on.
override in derived classes.
In C++23, override is your safety net: it makes the compiler reject “almost overrides” caused by
signature mismatches (a common source of subtle bugs).
struct Shape {
virtual ~Shape() = default;
virtual void draw() const = 0;
};
struct Circle final : Shape {
void draw() const override; // checked override
};
Rule of thumb: if a class is intended to be used polymorphically (i.e., has virtual functions), its destructor should almost always be virtual.
struct Base {
virtual ~Base() = default;
};
During base-class construction, the object’s dynamic type is the base. The derived subobject doesn’t exist yet, so dispatching to derived overrides would be unsafe.
During construction and destruction, virtual dispatch “collapses” toward the currently-constructed (or currently-destroyed) type. This can surprise readers and break invariants.
If you need “two-phase initialization,” prefer a named factory or builder that constructs the object and then calls a non-virtual initialization step on the fully-formed object, or invoke a virtual hook after construction via an external function.
A pure virtual (= 0) function expresses “every derived type must provide this behavior.”
Keep the interface small and cohesive; large “fat base classes” tend to become dumping grounds.
struct Reader {
virtual ~Reader() = default;
virtual std::string readLine() = 0;
};
A common C++ design is to expose a public non-virtual function that enforces pre/post conditions and calls a protected virtual implementation hook. This keeps invariants in one place while allowing customization.
class Document {
public:
void save() { // non-virtual public API
// validate invariants, logging, timing, etc.
saveImpl(); // customization point
}
virtual ~Document() = default;
protected:
virtual void saveImpl() = 0;
};
final when extension is not intended.
final documents intent and can enable devirtualization optimizations.
Callers typically reason about exceptions at the base interface level. If one override throws where others do not, document it clearly and consider whether the base interface should express that behavior.
Polymorphic objects are often created dynamically. In C++23, prefer RAII:
store owning pointers in std::unique_ptr<Base> by default, and use
std::shared_ptr only when shared ownership is truly required.
Virtual calls become interesting when you access a derived object through a base pointer or reference. The runtime selects the correct override based on the object’s dynamic type.
#include <iostream>
struct Base {
virtual ~Base() = default;
virtual void vfunc() const {
std::cout << "Base::vfunc()\n";
}
};
struct Derived1 : Base {
void vfunc() const override {
std::cout << "Derived1::vfunc()\n";
}
};
struct Derived2 : Base {
void vfunc() const override {
std::cout << "Derived2::vfunc()\n";
}
};
int main() {
Base b;
Derived1 d1;
Derived2 d2;
Base* p = &b;
p->vfunc();
p = &d1;
p->vfunc();
p = &d2;
p->vfunc();
}
This program displays the following:
Base::vfunc()
Derived1::vfunc()
Derived2::vfunc()
override, and make polymorphic base destructors virtual.unique_ptr by default) for polymorphic objects.final and small interfaces to keep hierarchies maintainable.