Polymorphism Cheatsheet¶
One interface, several behaviours — decided at run time from the object's real type. The rules below are short; the mistakes they prevent are the most expensive in the course.
Pairs with Module 7 — Polymorphism
The three annotations¶
class Shape
{
public:
virtual ~Shape() = default; // polymorphic base: virtual destructor
virtual double area() const = 0; // pure virtual: derived classes must provide it
virtual void describe(std::ostream& out) const;
};
class Circle : public Shape
{
public:
double area() const override { return pi_ * r_ * r_; } // override: compiler checks the match
};
| Annotation | Where | Meaning |
|---|---|---|
virtual |
Base declaration only | Call is resolved from the object's real type |
override |
Derived declaration | Verify that this really overrides something |
final |
Either | No further overriding |
= 0 |
Base declaration | Pure virtual — the class is abstract, and derived classes must implement it |
Always write override. Without it, a signature that differs by const silently declares a
new function instead of overriding, and your program calls the base version forever.
class Derived : public Base
{
public:
int value() override { return 2; } // base declares: virtual int value() const
};
error: 'int Derived::value()' marked 'override', but does not override
error: 'virtual int Base::value() const' was hidden [-Werror=overloaded-virtual=]
note: by 'int Derived::value()'
The missing const is the whole bug, and override is what turned it into a compiler error
instead of a wrong answer.
Static versus dynamic binding¶
| Binding | Chosen | Requires |
|---|---|---|
| Static (default) | At compile time, from the expression's declared type | — |
| Dynamic | At run time, from the object's real type | virtual, and a pointer or reference |
class Base { public: void speak() const { std::cout << "base speaks\n"; } }; // not virtual
class Derived : public Base { public: void speak() const { std::cout << "derived speaks\n"; } };
int main()
{
Derived d;
Base& ref = d;
ref.speak(); // static binding: prints "base speaks"
d.speak(); // direct call: prints "derived speaks"
}
Dispatch needs a pointer or a reference. Calling through a value uses the object's static type — that is exactly what slicing is.
Object slicing¶
Putting a derived object into a base-class value copies only the base part. The derived data and behaviour are gone, and nothing warns you.
std::vector<Animal> by_value;
by_value.push_back(Dog());
by_value[0].speak(); // prints the base behaviour
The first line is the sliced Animal; the second is the same Dog reached through a pointer.
Hold polymorphic objects by pointer, never by value:
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));
for (const auto& s : shapes) { std::cout << s->area() << '\n'; } // dispatches
The virtual destructor rule¶
Any class with a virtual function needs a virtual destructor. Without it, delete through a
base pointer runs only the base destructor — the derived members are never released, and there is
no warning and no error.
class Base
{
public:
Base() { ++alive; }
~Base() { --alive; } // NOT virtual
};
class Derived : public Base
{
public:
Derived() { ++derived_alive; }
~Derived() { --derived_alive; }
};
The derived object is still "alive" after being deleted: its destructor never ran. The fix is one
word — virtual ~Base(); — and the reason is that delete on a base pointer cannot know the real
type unless the destructor is virtual.
Abstract classes and interfaces¶
| Concept | Rule |
|---|---|
| Abstract class | Has at least one pure virtual function; cannot be instantiated |
| Interface | An abstract class with no data members and only pure virtual functions, plus a virtual destructor |
| Concrete class | Overrides every inherited pure virtual function, and therefore can be created |
| Destructor of an interface | Must be virtual (and can be = default) |
error: cannot declare variable 's' to be of abstract type 'Shape'
note: because the following virtual functions are pure within 'Shape':
note: 'virtual double Shape::area() const'
The second note names exactly which function is still unimplemented — read it, it is the fix.
Virtual calls during construction and destruction¶
A virtual call in a constructor or destructor is resolved to the class being constructed or
destroyed, not the real type. During Base's constructor, the object is a Base; the
derived part does not exist yet.
class Base { public: Base() { speak(); } virtual void speak() const { std::cout << "base\n"; } };
class Derived : public Base { public: void speak() const override { std::cout << "derived\n"; } };
Constructing a Derived prints base. If you need derived behaviour at construction, pass the
data in and do the work in a separate init()-style call from the caller — not from the
constructor.
Checklist¶
- [ ] Does every polymorphic base have a
virtual ~? - [ ] Does every overriding function say
override? - [ ] Are polymorphic objects held by pointer or reference — never by value?
- [ ] Is anything deleted through a base pointer that lacks a virtual destructor?
- [ ] Are the base's functions meaningful for every derived type (Liskov)?
- [ ] Is there any virtual call from a constructor or destructor?
- [ ] Is the interface free of data members?
Related: Inheritance · Common OOP Bugs · SOLID · Module 7 — Polymorphism