Language[lang]

Polymorphism in C++: one call, many behaviors

Section
Language [lang]
Standard
C++17
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
7 min read
In this answer6 clauses
  1. 1What makes a call polymorphic in C++?
  2. 2What is runtime polymorphism in C++?
  3. 3What is compile-time polymorphism in C++?
  4. 4What are the common polymorphism mistakes?
  5. 4.1Slicing: a derived object passed by value
  6. 4.2A base function that is not virtual
  7. 4.3A base destructor that is not virtual
  8. 5What is an abstract class in C++?
  9. 6Compile-time vs runtime polymorphism

Abstract

Polymorphism in C++ lets one call run different code for different types. Runtime polymorphism is a virtual function called through a base-class pointer or reference: the class of the object decides which version runs, and override makes the compiler check each one. Compile-time polymorphism is function overloading and templates: the compiler picks the code from the argument types. Hold polymorphic objects by pointer or reference, not by value, and give the base class a virtual destructor when derived objects are deleted through a pointer to it.

A small square cell with a dot sends one line to the right that forks into three arrows: the top one ends at an empty square, the middle blue one at a hatched circle, the bottom one at a triangle.
Figure 1 — One virtual call through a base-class pointer reaches objects of several classes, and each runs its own version of the function.

Polymorphism in C++ means that one call can run different code depending on the type it is used with. At run time, a call to a virtual function through a base-class pointer or reference runs the version that belongs to the object’s real class. At compile time, overloaded functions and templates pick the code from the argument types.

Two shapes in a list, as a whole program:

[Example 1:
#include <iostream>
#include <memory>
#include <vector>

constexpr double pi = 3.14159265358979;

struct Shape {
    virtual double area() const { return 0.0; }
    virtual ~Shape() = default;
};

struct Circle : Shape {
    explicit Circle(double r) : radius{r} {}
    double area() const override { return pi * radius * radius; }
    double radius;
};

struct Square : Shape {
    explicit Square(double s) : side{s} {}
    double area() const override { return side * side; }
    double side;
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(1.0));
    shapes.push_back(std::make_unique<Square>(2.0));

    for (const auto& shape : shapes) {
        std::cout << shape->area() << '\n';  // one call, two results
    }
}

— end example]

It printed:

[Example 2:
3.14159
4

— end example]

Circle and Square are derived classes of Shape, the base class: they inherit from it, and each overrides area, replacing the base’s version with its own (inheritance in C++ covers what else a derived class gets). The vector holds std::unique_ptr<Shape>, smart pointers to the base that delete their objects. The loop makes one call, shape->area(), and each object runs its own area.

The samples are C++17, built with MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2 and Clang 23.1 (libc++), and all four printed the same. override dates from C++11 (Since When lists the first compiler releases with it).

What makes a call polymorphic in C++?

Three things, all at once:

  • the function is declared virtual in the base class;
  • the derived class overrides it, with the same name, parameters and const;
  • the call goes through a pointer or a reference to the base, as shape->area() does.

Then the call runs the version that belongs to the object’s own class, its dynamic type, rather than to the type the pointer was declared with, its static type ([class.virtual]).

Two rows, each a pointer cell labelled Shape star shape pointing to an object: a hatched circle labelled Circle, then a hatched square labelled Square. The same call, shape arrow area, lands on Circle::area, then Square::area.
Figure 2 — When area is virtual, the same call through a Shape pointer runs the version that belongs to the class of the object it points at.

Take virtual off Shape::area, and override off the two derived functions, and the same loop runs the base version both times:

[Example 3:
struct Shape {
    double area() const { return 0.0; }  // no virtual
    virtual ~Shape() = default;
};
// ...
for (const auto& shape : shapes) {
    std::cout << shape->area() << '\n';  // Shape::area both times
}

— end example]

It printed:

[Example 4:
0
0

— end example]

Without virtual, the type the pointer was declared with decides, and that type is Shape. All four compilers built it without a warning.

What is runtime polymorphism in C++?

Runtime polymorphism in C++, also called dynamic or late binding, is a virtual function called through a base-class pointer or reference. Which version runs is decided while the program runs, from the object it points at, so the same loop works for any class derived from Shape. Write override on each function that overrides, so the compiler rejects one that does not match the base. This area forgets const, so it overrides nothing:

[Example 5:
struct Circle : Shape {
    explicit Circle(double r) : radius{r} {}
    // error: const is missing, so this overrides nothing
    double area() override { return pi * radius * radius; }
    double radius;
};

— end example]

All four rejected it. GCC said:

[Example 6:
error: 'double Circle::area()' marked 'override', but does not override

— end example]

A function marked override that overrides nothing does not compile, and neither does overriding a function marked final. The derived function is virtual whether or not it says so; the C++ Core Guidelines (C.128) ask for exactly one of virtual, override and final on each virtual function.

The C++ virtual functions answer goes further: pure virtual functions with a body and virtual calls in constructors.

What is compile-time polymorphism in C++?

Compile-time polymorphism, also called static polymorphism, is the compiler choosing the function from the types in the call. Function overloading and templates do it, with no base class and no virtual.

Function overloading is one name with several parameter lists. Overload resolution, the compiler’s matching step, picks the function whose parameters match the arguments most closely:

[Example 7:
void print(int value) {
    std::cout << "print(int): " << value << '\n';
}

void print(double value) {
    std::cout << "print(double): " << value << '\n';
}

void print(const std::string& value) {
    std::cout << "print(string): " << value << '\n';
}
// ...
const std::string name = "square";
print(4);        // print(int)
print(3.14159);  // print(double)
print(name);     // print(string)

— end example]

It printed:

[Example 8:
print(int): 4
print(double): 3.14159
print(string): square

— end example]

Only the parameters count: two functions that differ in their return type alone cannot overload each other. Overloaded operators such as + and << are chosen the same way (operator overloading in C++).

A function template is one function written once for many types. The compiler writes a separate function, an instantiation, for each set of argument types the template is called with:

[Example 9:
template <typename T>
T larger(T a, T b) {
    return a > b ? a : b;
}
// ...
const std::string circle = "circle";
const std::string square = "square";
std::cout << larger(2, 4) << '\n';             // T is int
std::cout << larger(1.5, 0.5) << '\n';         // T is double
std::cout << larger(circle, square) << '\n';   // T is std::string

— end example]

It printed 4, 1.5 and square.

What are the common polymorphism mistakes?

Three, and two of them compiled without a warning on all four compilers.

Slicing: a derived object passed by value

Passing or storing a Circle by value as a Shape copies only its Shape part, which is called slicing, and the copy runs Shape::area:

[Example 10:
void describe(Shape shape) {  // a copy of the Shape part only
    std::cout << shape.area() << '\n';
}

void describe_ref(const Shape& shape) {  // the Circle itself
    std::cout << shape.area() << '\n';
}
// ...
Circle circle{1.0};
describe(circle);      // 0
describe_ref(circle);  // 3.14159

— end example]

It printed 0, then 3.14159, and none of the four compilers warned about describe(circle). Take polymorphic objects by reference or pointer. By value, in the Core Guidelines’ words, “only the base portion of a derived object will be copied, and the polymorphic behavior will be corrupted” (C.67).

A base function that is not virtual

Leave virtual out of the base class and every call through a Shape pointer runs Shape::area, as the loop without virtual printed, again with no warning. Writing override on the derived functions catches it: with no virtual area in the base to override, all four compilers rejected them.

A base destructor that is not virtual

delete through a Shape* runs both destructors when the base class’s destructor is virtual:

[Example 11:
struct Shape {
    virtual double area() const { return 0.0; }
    virtual ~Shape() { std::cout << "~Shape\n"; }
};

struct Circle : Shape {
    explicit Circle(double r) : radius{r} {}
    double area() const override { return pi * radius * radius; }
    ~Circle() { std::cout << "~Circle\n"; }
    double radius;
};
// ...
Shape* shape = new Circle{1.0};
delete shape;  // ~Circle, then ~Shape

— end example]

It printed ~Circle, then ~Shape. Without virtual on ~Shape, the same delete is undefined behavior, which means the standard sets no rule for what the program does ([expr.delete]), so that version was compiled and never run. GCC and both Clangs warned about it; MSVC said nothing at /W4. The running example’s virtual ~Shape() = default; is the fix (C++ Core Guidelines C.35).

What is an abstract class in C++?

An abstract class is a class with at least one pure virtual function, declared with = 0: you cannot create an object of it, only of derived classes that override that function. In the first example Shape::area has a body that returns 0; make it pure, and Shape shape; stops compiling:

[Example 12:
struct Shape {
    virtual double area() const = 0;  // pure virtual: no body
    virtual ~Shape() = default;
};
// ...
Shape shape;  // error: Shape is abstract

— end example]

All four rejected it. Clang said:

[Example 13:
error: variable type 'Shape' is an abstract class

— end example]

Pointers and references to an abstract class are still allowed, and they are all that runtime polymorphism needs.

Compile-time vs runtime polymorphism

Question Compile-time Runtime
Decided by the compiler, from the argument types while the program runs, from the object
Written with overloaded functions, templates virtual functions and override
Types known every type, when the call is compiled the base class when compiled, the object’s class only when it runs
Common base class not needed required
Typical use one operation on unrelated types: print, larger different objects behind one interface: the vector of shapes

A std::variant read with std::visit (both C++17) is another way, for a fixed set of types that need no common base: std::visit calls a function with the value the variant holds. More answers about the core language are in Language.

Questions

What are the types of polymorphism in C++?

There are two. Compile-time polymorphism is function overloading and templates: the compiler picks the function from the argument types. Runtime polymorphism is a virtual function called through a base-class pointer or reference: the class of the object it points at decides which version runs.

Is function overloading polymorphism?

Yes, the compile-time kind. Several functions share one name and differ in their parameters, and the compiler picks the one whose parameters match the arguments most closely, so print(4) and print(3.14159) call different functions. The return type plays no part in the choice.

What is the difference between overloading and overriding?

Overloading is several functions with one name and different parameters; the compiler picks one from the argument types. Overriding is a derived class replacing a virtual function of its base class with its own version, with the same name, parameters and const; called through a base-class pointer or reference, which version runs is decided at run time, from the object. Write override on an overriding function so the compiler checks that it really overrides one.

Why does polymorphism need a pointer or a reference?

An object held by value has exactly the type it was declared with: a Shape variable is a Shape, even when it was copied from a Circle, and the copy holds only the Shape part. A pointer or a reference to Shape can refer to a whole Circle, so a virtual call through it runs Circle::area.

Can you have polymorphism without virtual functions?

Not the runtime kind through a base class: without virtual, a call through a Shape pointer runs Shape::area for every object. Templates and function overloading give compile-time polymorphism with no virtual function, and a std::variant read with std::visit (C++17) runs the code for whichever type it holds.

Does a polymorphic class need a virtual destructor?

Yes, when its objects are deleted through a pointer to the base class: without a virtual destructor, that delete is undefined behavior. The C++ Core Guidelines (C.35) put the rule as: a base class destructor should be either public and virtual, or protected and non-virtual.

Read next

  1. Inheritance in C++: base and derived classesLanguage · Miriam Castellanos · written for C++17
  2. C++ virtual functions: override, pure virtual and the trapsLanguage · Miriam Castellanos · written for C++11
  3. C++ while loop (and do-while): syntax and examplesLanguage · Miriam Castellanos · written for C++17