OOP · Objects & interfaces

Polymorphism

One interface can call behavior supplied by different object types.

Ask for the behavior, not the type.

Runtime polymorphism lets one interface work with different object types. Code can ask an Animal to make a sound without deciding whether it holds a Dog or Cat. In an ordinary virtual call through a base pointer or reference, the actual object determines which override runs.

Callers need fewer type-specific branches.

If every caller checks “is this a Dog?” before choosing a sound, adding Cat requires editing those callers. A virtual operation puts each type’s behavior behind the shared interface. Inheritance establishes the type relationship; virtual dispatch chooses the behavior for this call.

The pointer type and object type differ.

animal has type Animal*, but it points to the existing Dog object. sound is virtual, so animal->sound() calls Dog::sound and returns “woof”. Automatic objects keep this example focused on dispatch without requiring manual allocation.

struct Animal {
    virtual const char* sound() const { return "?"; }
    virtual ~Animal() = default;
};
struct Dog : Animal {
    const char* sound() const override { return "woof"; }
};
Dog dog;
Animal* animal = &dog;
std::cout << animal->sound();

Interface → object → override.

Animal* lets the caller use Animal’s interface. The Dog object supplies the final override for sound. The pointer does not turn the Dog into an Animal-only value; it continues to refer to the original Dog.

An Animal pointer refers to a Dog object; the virtual sound call selects Dog::sound.
Animal* animal
shared interface
Dog object
actual object type
Dog::sound()
returns “woof”
  • Animal* animal leads to Dog object (points to)
  • Dog object leads to Dog::sound() (dispatches)

Related types and selected behavior are different ideas.

Inheritance gives Dog an Animal base part and a base interface. Runtime polymorphism uses virtual functions to choose an implementation through that interface. Inheriting a non-virtual function alone does not make calls dynamically select a derived function.

Without virtual, the expression’s type selects the call.

If sound in Animal is non-virtual, a call through Animal* selects Animal::sound. Dog’s same-named function does not change that. Remove override from Dog too when trying this comparison: override on a non-overriding function is a compile-time error.

Copying a base value loses the derived part.

Animal animal = dog creates a separate Animal object by copying the Animal base part. The new object is an Animal, so its ordinary virtual sound call uses Animal::sound. Keeping a base reference or pointer avoids this slicing because it refers to the original Dog.

Dog dog;
Animal animal = dog; // separate Animal value
std::cout << animal.sound(); // "?"

Further reading

Trusted resources for the rules and APIs discussed in this guide.