Run-time Polymorphism

Learn how to write flexible and extensible C++ code using polymorphism, virtual functions, and dynamic casting
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

This lesson is a quick introductory tour of polymorphism within C++. It is not intended for those who are entirely new to programming. Rather, the people who may find it useful include:

  • those who have completed our introductory course, but want a quick review
  • those who are already familiar with programming in another language, but are new to C++
  • those who have used C++ in the past, but would benefit from a refresher

It summarises several lessons from our introductory course. Those looking for more thorough explanations or additional context should consider completing Chapter 6 of that course.

Previous Course

Intro to Programming with C++

Starting from the fundamentals, become a C++ software engineer, step by step.

Screenshot from Cyberpunk 2077
Screenshot from The Witcher 3: Wild Hunt

Early Binding

This lesson relates to an important concept that occurs when we’re working with objects from an inheritance hierarchy.

Let’s imagine we have a Monster object. Specific subtypes of Monster inherit from it, and provide their own implementation of the Log method:

class Monster {
public:
  void Log(){ std::cout << "Monster\n"; }
};

class Dragon : public Monster {
public:
  void Log(){ std::cout << "Dragon\n"; }
};

We have functions and other systems that work with this hierarchy, and we’d like them to call the version of the function defined for the specific type of object they receive.

That is, we’d like a function like the following to call Monster::Log() if provided with a reference to a basic Monster, but Dragon::Log() if provided specifically with a Dragon:

void CallMethod(Monster& Object){
  Object.Log();
}

If we test our code we’ll see that, by default, this does not happen.

When calling methods through references and pointers, C++ uses early binding by default. That means, the compiler figures out what methods to call at compile time, based on the type defined within the parameter list, even if our object is a more specific subtype:

#include <iostream>

class Monster {
public:
  void Log(){ std::cout << "Monster\n"; }
};

class Dragon : public Monster {
public:
  void Log(){ std::cout << "Dragon\n"; }
};

void CallMethod(Monster& Object){
  Object.Log();
}

int main(){
  Monster BaseObject;
  std::cout << "The first object is a ";
  CallMethod(BaseObject);

  Dragon DerivedObject;
  std::cout << "The second object is a ";
  CallMethod(DerivedObject);
}
The first object is a Monster
The second object is a Monster

Even though the second invocation of CallMethod is passed a reference to a Dragon object, our program logs "Monster" on both invocations.

This is because the reference in the CallMethod function has the type of Monster, and the Monster::Log function uses early binding. Early binding means the compiler identifies what function to call at compile time, based on the type defined within the parameter list.

In this case, the type within the CallMethod function was a Monster reference, Monster&, so the compiler binds the Object.Log() call to the version of that function that is in the Monster class.

Late Binding with virtual

To flag a method for late binding, we need to add the virtual specifier to it:

class Monster {
public:
  virtual void Log(){
    std::cout << "Monster\n";
  }
};

This has performance implications, as our program now needs to find the correct function at run time.

But, because it happens at run time, our program can investigate exactly what is stored in that location in memory at the point our function is called. It identifies the second call as receiving not just a plain old Monster object, but specifically a Dragon object.

As a result, it calls the more specific function, which was in the Dragon class. With no further changes to our program, we get the desired output:

The first object is a Monster
The second object is a Dragon

This is an example of a powerful run-time polymorphism a powerful concept that lets our project expand in complexity, without our systems getting more complex.

Our core system (in this case, the contrived CallMethod() function) has not changed. In fact, that system doesn’t even know there is a class called Dragon. Yet, they can interact with Dragon objects, invoking all the dragon-specific behaviors.

We could add 100 more types of Monster, and our core systems don’t need to change at all to accommodate all that variation. They just call Monster methods, and our specific Monster subtypes get to decide what happens, by overriding those methods as required.

The override keyword

When a child class is overriding a function it is inheriting, we should add the override specifier to that function:

class Dragon : public Monster {
public:
  void Log() override {
    std::cout << "Dragon" << endl;
  }
};

This is optional, but it is highly recommended. Adding the override specifier makes our intent clear, both to other developers and the compiler.

If we attempt to override a function, but we misspell its name, or provide a different argument list, the compiler doesn’t know we’ve made a mistake. It assumes we’re just defining a different function, so it lets the bug slip through.

By adding the override specifier, the compiler will know our intent and will throw an error if the function we’re defining on the derived class is not actually overriding anything on a base class.

Downcasting with dynamic_cast

When we have a pointer or reference of the base type, and we want to access a function or variable on a more derived type, we can use dynamic_cast:

In this method, we have a parameter of the type Monster&, but we suspect it might be a more specific Dragon object, so we downcast it appropriately:

void CallMethod(Monster& Object) {
  dynamic_cast<Dragon&>(Object);
}

To use dynamic_cast, our type must be polymorphic, that is, it must define at least one virtual method. If we otherwise have no need for any virtual method in the class, conventionally we just mark the destructor as virtual:

class Monster {
public:
  virtual ~Monster(){}
};

Dynamic casting will succeed if the reference or pointer really is the derived type we tried to cast it to. In that successful scenario, it will return a reference or pointer to that same memory location, but with an updated type. This allows us to access derived members:

#include <iostream>

class Monster {
public:
  virtual void BaseFunction(){
    std::cout << "Monster ";
  }
};

class Dragon : public Monster {
public:
  void DerivedFunction(){
    std::cout << "and Dragon";
  }
};

void CallMethod(Monster& Obj){
  Obj.BaseFunction();
  dynamic_cast<Dragon&>(Obj)
    .DerivedFunction();
}

int main(){
  Dragon DerivedObject;
  CallMethod(DerivedObject);
}
Monster and Dragon

Dynamic casting can fail. This happens when the object doesn’t actually have the type we’re trying to cast it to. Below, we’re trying to dynamic cast to a Dragon, but what we’ve received is just a plain old Monster:

void CallMethod(Monster& Object) {
  dynamic_cast<Dragon&>(Object);
}

int main() {
  Monster BaseObject;
  CallMethod(BaseObject);
}

If dynamic casting a reference fails, as in the above example, our code will throw an exception. We cover exceptions and how to handle them later in this course.

If dynamic casting a pointer fails, it returns a nullptr. We can test for null pointers by using an if statement:

#include <iostream>

class Monster {
public:
  virtual ~Monster(){}
};

class Dragon : public Monster {};

void CallMethod(Monster* Obj){
  Dragon* DragonPtr{
    dynamic_cast<Dragon*>(Obj)
  };
  if (DragonPtr) {
    std::cout << "That's a dragon\n";
  } else {
    std::cout << "That isn't";
  }
}

int main(){
  Dragon Scaly;
  CallMethod(&Scaly);

  Monster Goblin;
  CallMethod(&Goblin);
}
That's a dragon
That isn't

Summary

Polymorphism is a powerful concept that allows for flexible and extensible code. By utilizing virtual functions and dynamic casting, we can write code that adapts to different object types at runtime. Key takeaways:

  • Polymorphism enables code to interact with objects of derived classes through base class references or pointers
  • Virtual functions are bound at runtime, allowing the most derived implementation to be called
  • The override keyword helps ensure that derived classes properly override base class functions
  • dynamic_cast allows for safe downcasting of polymorphic objects, enabling access to derived class members

Was this lesson useful?

Next Lesson

Odds and Ends: 10 Useful Techniques

A quick tour of ten useful techniques in C++, covering dates, randomness, attributes and more
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

Run-time Polymorphism

Learn how to write flexible and extensible C++ code using polymorphism, virtual functions, and dynamic casting

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
  • 45.GPUs and Rasterization
  • 46.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 47 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Odds and Ends: 10 Useful Techniques

A quick tour of ten useful techniques in C++, covering dates, randomness, attributes and more
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved