Inheritance & Polymorphism
What a virtual function actually costs, and where inheritance stops being free.
Intermediate
| Overloading (static polymorphism) | Overriding (dynamic polymorphism) | |
|---|---|---|
| Resolved | at compile time, by argument types | at runtime, by the object's actual type |
| Requires inheritance | no | yes, plus a virtual function |
| Scope | same scope — multiple functions with the same name coexist | different scopes — a derived class's version replaces the base's for dynamic dispatch |
| Signature | must differ (parameter count or types) | must match exactly (same return type and parameters) |
override doesn't do anything at runtime — it's a
compile-time check that the function actually overrides a virtual function with a matching
signature in the base class. Without it, a typo in the signature (wrong parameter type, a
missing const) silently creates an unrelated overload instead of an override,
and the bug only shows up as "why isn't my override being called" at runtime.
class NoVirtual { int *ptr; float f; bool b; void print(){} }; // 16 bytes — no vptr
class WithVirtual { int *ptr; float f; bool b; virtual void print(){} }; // 24 bytes — +8 for the vptr
| Cost | What it is |
|---|---|
| 1. Vtable storage | one array per class with virtual functions — negligible unless you have hundreds of classes with many virtual functions each |
| 2. Vptr per object | +8 bytes per instance, whether or not that instance ever calls a virtual function |
| 3. No inlining | the real cost — a call through a base pointer/reference can't be resolved until runtime, so the compiler can't inline it even when it safely could for a non-virtual call |
| 4. RTTI | one type_info object per class plus one vtable slot to point at it — doesn't grow individual objects, only the vtable |
class Base { public: ~Base() { std::cout << "~Base\n"; } }; // NOT virtual
class Derived : public Base { public: ~Derived() { std::cout << "~Derived\n"; } };
Base *p = new Derived();
delete p; // prints only "~Base" — ~Derived never runs. Any Derived-owned resources leak.
class Base { public: virtual ~Base() { std::cout << "~Base\n"; } }; // virtual fixes it
class Derived : public Base { public: ~Derived() { std::cout << "~Derived\n"; } };
Base *p = new Derived();
delete p; // prints "~Derived" then "~Base" — both run, in the correct order
// PoweredDevice
// / \
// Scanner Printer
// \ /
// AllInOnePrinter
class PoweredDevice { public: PoweredDevice() { std::cout << "PoweredDevice ctor\n"; } };
class Scanner : public PoweredDevice {};
class Printer : public PoweredDevice {};
class AllInOnePrinter : public Scanner, public Printer {}; // now has TWO PoweredDevice subobjects
AllInOnePrinter printer; // prints "PoweredDevice ctor" twice — printer.Scanner::??? and printer.Printer::??? refer to different PoweredDevices
virtual in both intermediate classes collapses the
two copies back into one, and shifts responsibility for constructing that shared base to the
most-derived class — even a parameterized PoweredDevice constructor
called by Scanner or Printer is ignored unless
AllInOnePrinter calls it directly.
class PoweredDevice { public: PoweredDevice() { std::cout << "PoweredDevice ctor\n"; } };
class Scanner : virtual public PoweredDevice {};
class Printer : virtual public PoweredDevice {};
class AllInOnePrinter : public Scanner, public Printer {};
AllInOnePrinter printer; // prints "PoweredDevice ctor" once — a single shared subobject
virtual — the object doesn't exist
yet, so there's no vptr to dispatch through. The "virtual constructor" idiom achieves the
same practical goal (pick the concrete type to create based on runtime information) through
a static factory function instead, decoupling the caller from knowing which concrete class
it's getting:
class Shape {
public:
static Shape *create(int id); // the "virtual constructor" — really a factory function
virtual ~Shape() = default; // still needs a real virtual destructor
virtual double area() const = 0;
};
class Circle : public Shape { public: double area() const override { return 3.14159; } };
class Square : public Shape { public: double area() const override { return 4.0; } };
Shape *Shape::create(int id) {
if (id == 0) return new Circle();
if (id == 1) return new Square();
return nullptr;
}
Shape *s = Shape::create(0); // caller never names Circle or Square directly
class Shape {
public:
virtual double area() const = 0; // pure virtual — no body, makes Shape abstract
virtual ~Shape() = default;
};
// Shape s; // error: cannot instantiate an abstract class
Shape *p = new Circle(); // fine — a concrete derived class can be instantiated
Shape s = Circle(); specifically fails to compile
because Shape is abstract — you can't instantiate it at all, by value or
otherwise. But the deeper reason polymorphism only works through a pointer or reference holds
even for non-abstract base classes: Base b = Derived(); would compile fine, but
it slices the object down to just its Base part, silently discarding the
derived-class data and any overridden behavior — no virtual dispatch happens on a
by-value object.