Basically, polymorphism is a base pointer or reference managing a derived class.
Assuming an inheritance hierarchy as shape as the base class and circle and square for the derived classes we can do the following:
Shape *circle = new Circle;
Shape *square = new Square;
We can even store them in an array:
Shape *shapes[] {circle, square};
and then process the different types using a loop calling the appropriate methods for the derived type::
for (Shape shape: shapes) {
shape.draw();
}
What we can’t do is call any methods or use any attributes declared in circle or square.
By default, we don’t get polymorphism, instead we get static binding. This is binding done at compile time and in our loop above, we would always get the shape draw method called. In other words, the compiler using the type of the pointer to decide which method to call. The same happens if we use references instead of pointers.
Dynamic binding, that’s binding done at runtime, gives us polymorphism. We achieve that by marking our function declarations as virtual.
virtual void draw();
It is not necessary to mark derived classes functions as virtual as they are implicitly virtual. It is regarded as a best practice though. If you have virtual functions, you should mark your destructor as virtual also. If they are not marked as virtual, the compiler uses static binding to determine which destructors to call so if you have a pointer to a base class, only the base classes destructor will be called.
Polymorphism is not free however, we will pay a price in memory use. Dynamic binding can use a lot more memory that static binding as the compiler has to keep track of the functions to call at runtime.
It is easy to accidentally not override a method in a class hierarchy by making a typo and this leads to polymorphism not being used. You can catch errors like these by adding the override specifier to you method declarations:
virtual void plot() const override;
The overridden method must exist in a parent class and be marked virtual.
Overloading overridden functions has a nice twist. Consider the following code:
Parent class
virtual void plot() const;
virtual void plot(int colour_depth) const;
Child class
virtual void plot() const override;
The child class will not have access to the other plot method. Overriding one hides them all.
If we have a static member in the base class, derived classes share that member. It can be accessed using derived.static_member, This may be a problem if you want the derived class to have its own. In such a case simply re-declare the static member in the derived class. It will be polymorphic.
The final specifier can be used to prevent derived classes from overriding methods.
virtual void plot() const override final;
as well as preventing classes from being sub-classed
class ThreeDPoint final : public Point {
If you have your virtual functions under different access modifiers, the access modifier applied will be the declared type. So if you have the following code:
Base class
public:
virtual void plot() const;
Derived class
private:
virtual void plot() const override;
and you access it through the derived class it will be inaccessible but if you go through a pointer to the base class, it will be accessible.
Using default parameters with polymorphism can give weird results because the default parameters are decided at compile time using static binding but the function called is decided at runtime using dynamic binding. Default parameters with virtual functions are best avoided.
Polymorphic functions shouldn’t be called from constructors or destructors because the polymorphic function may not exist. The compiler instead uses static binding.