Runtime Polymorphism Recap

Inheritance let a derived class reuse a base class. This chapter added the part that makes inheritance powerful: choosing behavior from the object's actual type at run time, rather than from the type of the pointer or reference holding it.

Why Base Pointers and References Matter

A base class pointer or reference can refer to a derived object. That is what lets one function, or one array, handle every type derived from a common base without knowing which is which.

On its own, though, it is disappointing. Through a base class pointer or reference, a non-virtual member function call reaches the base class version, even when the object is really a derived one. The pointer's type decides, and the object's type is ignored.

Virtual Functions

A virtual function changes that rule: the call resolves to the most-derived version that exists between the base class and the actual type of the object. That derived version is an override.

For a function to override, its signature and return type must match the base class version. The one relaxation is covariant return types: when the base returns a pointer or reference to the base class, an override may return a pointer or reference to the derived class.

Two specifiers make intent explicit and let the compiler check it. The override specifier says a function is meant to override, so a typo in the name or a mismatched parameter becomes a compile error instead of a silently separate function. The final specifier does the opposite, preventing further overrides of a function or further inheritance from a class.

#include <iostream>
#include <string>

class Instrument
{
public:
    virtual ~Instrument() = default;

    virtual std::string name() const { return "Instrument"; }
};

class Trumpet : public Instrument
{
public:
    std::string name() const override { return "Trumpet"; }
};

void report(const Instrument& instrument)
{
    std::cout << instrument.name() << '\n';
}

int main()
{
    Trumpet trumpet{};
    report(trumpet);

    Instrument sliced{ trumpet };
    report(sliced);

    return 0;
}

Output:

Trumpet
Instrument

The first call goes through a const Instrument& bound to a Trumpet and reaches the derived version. The second demonstrates the trap covered below.

Virtual Destructors

If a class is meant to be inherited from, give it a virtual destructor. Deleting a derived object through a base class pointer with a non-virtual destructor does not run the derived destructor, so whatever the derived class owns is never released. The recommendation for any base class in a hierarchy is a destructor that is both virtual and public.

Bypassing Virtual Dispatch

The scope resolution operator overrides the override. Writing storage.Storage::getType() calls that specific class's version regardless of the object's actual type, which is occasionally what you want when a derived implementation needs to extend rather than replace the base behavior.

How It Works, and What It Costs

Early binding happens when the compiler sees a direct call and can resolve the target during compilation. Late binding defers the decision to run time, which is what a call through a function pointer requires.

Virtual functions use late binding, implemented with a virtual table. Each polymorphic class has a table of pointers to the correct implementations for that class, and each object carries a pointer to its class's table. A virtual call reads that pointer, finds the entry, and calls it.

That machinery is not free, and the cost comes in two parts:

  • Calls take longer, because the target is looked up rather than known
  • Every object of a class with virtual functions grows by one pointer

Both are small, and both are worth knowing about before adding virtual to something instantiated in enormous quantities.

Abstract Classes and Interfaces

Adding = 0 to a virtual function's declaration makes it pure virtual, also called abstract. A class holding at least one pure virtual function is an abstract class and cannot be instantiated: it exists to be inherited from. A derived class that does not define every inherited pure virtual function is itself abstract. A pure virtual function may still have a body, which serves as a default implementation a derived class can call, and the class stays abstract regardless.

An interface class takes this to its conclusion: no member variables, and every function pure virtual. It specifies what implementations must provide without dictating anything about how. The convention is to name them starting with a capital I.

Virtual Base Classes

When the same base class is inherited more than once through different paths, a virtual base class ensures only one copy of it exists in the final object rather than one per path.

Object Slicing

Assigning a derived object to a base class object, rather than to a base pointer or reference, copies only the base portion. The derived part is discarded. This is object slicing, and it is what the second call in the program above demonstrates: sliced is a genuine Instrument, not a Trumpet wearing an Instrument label, so it reports Instrument. Working through pointers and references is what avoids it.

Dynamic Casting

Dynamic casting converts a base class pointer to a derived class pointer, which is downcasting. Unlike static_cast, it checks at run time whether the object really is of the target type, and returns a null pointer when the conversion is not valid. Reach for dynamic_cast whenever you are downcasting and cannot prove the type in advance, and check the result before using it.

Printing Inherited Classes

operator<< cannot be virtual, since it is not a member function. The usual solution is to overload operator<< once for the most-base class and have it call a virtual member function that does the actual printing, which gets virtual behavior out of a non-virtual operator.

Terms Used in This Chapter

  • Virtual function: resolves to the most-derived version at run time
  • Override: a derived class function replacing a virtual base class function
  • override specifier and final specifier: assert that a function overrides, or forbid further overriding and inheritance
  • Covariant return type: an override returning a derived pointer or reference where the base returns a base one
  • Virtual destructor: needed so deletion through a base pointer runs the derived destructor
  • Early binding and late binding: the call target resolved during compilation, or at run time
  • Virtual table: the per-class table of function pointers that makes late binding work
  • Pure virtual function: declared = 0, with no required implementation in that class
  • Abstract class: has at least one pure virtual function and cannot be instantiated
  • Interface class: no member variables, all functions pure virtual
  • Virtual base class: included only once no matter how many inheritance paths reach it
  • Object slicing: losing the derived portion by copying into a base class object
  • Dynamic casting and downcasting: a runtime-checked conversion from base to derived

Looking Forward

The rule worth carrying out of this chapter is to work through references and pointers when you want polymorphic behavior, and to make the base class destructor virtual the moment a class is designed for inheritance. Between them, those two habits avoid slicing and leaks, which are the two ways this machinery usually goes wrong in practice.