Polymorphism

Table of Contents


1. Design and polymorphism

So much of the design tricks and features we utilize in C++ and other object-oriented programming languages all stem from the concept of "do not repeat yourself". If you're writing the same set of code in multiple places, there is a chance that we could design the program so that we only need to write that code once.

c3_u12_Polymorphism_family.png

Polymorphism is a way that we can utilize pointers and something called vtables to have a family of classes (related by inheritance) and be able to write one set of code to handle interfacing with all of those family members. We have a family tree of classes, and we can write our program to treat all the objects as the parent class, but the program will decide which set of functions to call at run time.

Parent* myPtr = nullptr;
if      ( type == 1 ) { myPtr = new ChildA; }
else if ( type == 2 ) { myPtr = new ChildB; }

myPtr->Display();
delete myPtr;

Example: Quizzer and multiple question types

Let's say we are writing a quiz program and there are different types of questions: True/false questions, multiple choice, and fill-in-the-blank. They all have a common question string, but how they store their answers is different…

Question TrueFalseQuestion MultipleChoiceQuestion FillInQuestion
# m_question : string # m_question : string # m_question : string # m_question : string
  # m_answer : bool # m_options : string[4] # m_answer : string
    # m_correct : int  
+ bool AskQuestion() + bool AskQuestion() + bool AskQuestion() + bool AskQuestion()
+ void DisplayQuestion()   + void ListAllAnswers()  

How would you store a series of inter-mixed quiz questions in a program? Without polymorphism, you might think to just have separate vectors or arrays for all the questions:

vector<TrueFalseQuestion>       tfQuestions;
vector<MultipleChoiceQuestion>  mcQuestions;
vector<FillInQuestion>          fiQuestions;

Utilizing polymorphism in C++, we could simply store an array of pointers of the parent type:

vector<Question*> questions;

And then initialize the question as the type we want during creation:

questions.push_back(new TrueFalseQuestion);
questions.push_back(new MultipleChoiceQuestion);
questions.push_back(new FillInQuestion);

Since we are using the `new` keyword here, we would also need to make sure to `delete` these items at the end of the program:

for (auto& question : questions)
{
    delete question;
}

Other design considerations

When we're working with polymorphism in this way, we need to be able to treat each child as its parent, from a "calling functions" perspective. Each child can have its own unique member functions and variables, but when we're making calls to functions via a pointer to the parent type, the parent only knows about functions that it, itself, has.

Let's say that the `Question` class has a `DisplayQuestion()` function. Since all its children use `mquestion` in the same way and inherit this function, it will be fine to call it via the pointer.

ptrQuestion->DisplayQuestion(); // ok

But with a function that belongs to a child - not the parent's interface - we wouldn't be able to call that function via the pointer without casting.

ptrQuestion->ListAllAnswers();  // not ok

(static_cast<MultipleChoiceQuestion*>(ptrQuestion))->ListAllAnswers(); ; ok

You could, however, still call that `ListAllAnswers` function from within `MultipleChoiceQuestion`'s `DisplayQuestion` function, and that would still work fine…

bool MultipleChoiceQuestion::AskQuestion()
{
    DisplayQuestion();
    ListAllAnswers();
    // etc.
}

Still fuzzy? That's OK, this is just an overview; we're going to step into how all this works more in-depth next.


2. Reviewing classes and pointers

2.1. Review: Class inheritance and function overriding

Some things to remember about inheritance with classes:

  • Any public or protected members (functions and variables) are inherited by the child class. (e.g., m_question, DisplayQuestion(), and AskQuestion()).
  • A child class can override the a parent's function by declaring and defining a function with the same signature. (e.g., AskQuestion()).
  • If the child class doesn't override a parent's function, then when that function is called via the child object it will call the parent's version of that function. (e.g., DisplayQuestion()).

c3_u12_Polymorphism_questioninherit.png


2.2. Review: Pointers to class objects

You can declare a pointer to point to the address of an existing object, or use the pointer to allocate memory for one or more new instances of that class…

  • Pointer to existing address: myPtr = &existingQuestion;
  • Pointer to allocate memory: myPtr = new Question;

Then, to access a member of that object via the pointer, we use the -> operator, which is equivalent to dereferencing the pointer and then accessing a member:

  • Arrow operator: myPtr->DisplayQuestion();
  • Dereference and access: (*myPtr).DisplayQuestion();

3. Which version of the method is called?

Let's say we have several objects already declared:

Question q1, q2;
MultipleChoiceQuestion mc1;

We could create a Question* ptr that points to q1 or q2 or even mc1

Question* ptr;
ptr = &q1;  ; ok
ptr = &q2;  ; ok
ptr = &mc1; ; ok?

c3_u12_Polymorphism_questioninherit.png

And, any functions that the Question class and the MultipleChoiceQuestion class could be called from this pointer…

ptr->DisplayQuestion();

This is fine for any member methods not overridden by the child class. But, which version of the function is called if we used an overridden method?

ptr->AskQuestion();

c3_u12_Polymorphism_whichone.png

3.1. No virtual methods - Which AskQuestion() is called?

Let's say our class declarations look like this:

Question:

class Question
{
    public:
    bool AskQuestion();
    // etc.
};

MultipleChoiceQuestion:

class MultipleChoiceQuestion : public Question
{
    public:
    bool AskQuestion();
    // etc.
};

Here are the outputs we could have from using pointers in different ways:

A. Question* pointer, Question's AskQuestion() is called:

Question* ptr = new Question;
bool result = ptr->AskQuestion();

B. MultipleChoiceQuestion* pointer, MultipleChoiceQuestion's AskQuestion() is called:

MultipleChoiceQuestion* ptr = new MultipleChoiceQuestion;
bool result = ptr->AskQuestion();

C. Question* pointer, Question's AskQuestion() is called:

Question* ptr = new MultipleChoiceQuestion;
bool result = ptr->AskQuestion();

"Well, how is that useful at all? The function called matches the pointer data type!" - true, but we're missing one piece that allows us to call any child's version of the method from a pointer of the parent type…


3.2. Virtual methods - Which AskQuestion() is called?

Instead, let's mark our method with the virtual keyword:

Question:

class Question
{
    public:
    virtual bool AskQuestion();
    // etc.
};

MultipleChoiceQuestion:

class MultipleChoiceQuestion : public Question
{
    public:
    virtual bool AskQuestion();
    // etc.
};

Here are the outputs we could have from using pointers in different ways:

A. Question* pointer, Question's AskQuestion() is called:

Question* ptr = new Question;
bool result = ptr->AskQuestion();

B. MultipleChoiceQuestion* pointer, MultipleChoiceQuestion's AskQuestion() is called:

MultipleChoiceQuestion* ptr = new MultipleChoiceQuestion;
bool result = ptr->AskQuestion();

C. Question* pointer, MultipleChoiceQuestion's AskQuestion() is called:

Question* ptr = new MultipleChoiceQuestion;
bool result = ptr->AskQuestion();

With this, we can now store a list of Question* objects, and each question can be a different child class, but we can write one set of code to interact with each one of them.



4. Virtual methods, late binding, and the Virtual Table

By using the virtual keyword, something happens with our functions - it allows the pointer-to-the-parent class to figure out which version of the method to actually call, instead of just defaulting to the parent class' version. But how does this work?

The virtual keyword tells the compiler that the function called will be figured out later. By marking a function as virtual, it then is added to something called a virtual table - or vtable.

The vtable stores special pointers to functions. If a class contains at least one virtual function, then it will have its own vtable.

c3_u12_Polymorphism_vtables1.png

With the Question class, it isn't inheriting any methods from anywhere else so the vtable reflects the same methods it has. But, we also have the child class that inherits DisplayQuestion() and overrides AskQuestion().

c3_u12_Polymorphism_vtables2.png

Because of these vtables, we can then have our pointers reference this vtable when figuring out which version of a method to call. Doing this is called late binding or dynamic binding.


4.1. When should we use virtual?

Destructors should always be virtual.

If you're working with inheritance. By making your destructor virtual for each class in the family, you are ensuring that the correct destructor will be called when the object is destroyed or goes out of scope. If you don't make it virtual and utilize polymorphism, the correct destructor may not be called (i.e., Question's instead of MultipleChoiceQuestion's).

Constructors cannot be marked virtual

When the object is instantiated (e.g., ptr = new MultipleChoiceQuestion;) that class' constructor will be called already.

Not every function needs to be virtual.

It's all about design. Though generally, if you always want the parent's version of a method to be called, you wouldn't override that method in the child class anyway.


4.2. Designing interfaces with pure virtual functions and abstract classes

Polymorphism works best if you're designing a family of classes around some sort of interface that they will all share. In the C# language, there is an interface type that is available to you, but that's not here in C++, so we implement it via classes.

What is an Interface?

When we're designing a class to be an interface, the idea is that the user (or other programmers) will just see a set of functions it will interface with - none of the behind-the-scenes, how-it-works stuff.

Most of the devices we use have some sort of interface, hiding the more complicated specifics of how it actually works within a case. For example, a calculator has a simple interface of buttons, but if you opened it up you would be able to see its hardware and how everything is hooked up.

We use the same idea with writing software, where we expose some interface (in the form of the class' public methods) as how the "user" interacts with our class.

c3_u12_Polymorphism_questionmanager.png

A common design practice is to write the first base (parent) class to be a specification of this sort of interface that all its children will adhere to, and to ensure that each child class must follow the interface by using something that the compiler will enforce itself: pure virtual functions.

When working with our Quiz program idea, our base class is Question, which would define the interface for all other types of Questions. Generally, our base interface class would never be instantiated - it is not complete in and of itself (i.e., a Question with no types of Answers) - but is merely used to outline a common interface for its family members.

Here is a blank diagram with just the member variables defined, but not yet any functionality, so that we can begin to step through thinking about an interface:

c3_u12_Polymorphism_questionfamily.png

Thinking in terms of implementing a program that could edit questions (such as the teacher's view of the quiz), as well as that could ask questions (such as the student's view), we can try to think of what kind of functionality we would need from a question…

  • Setup the question, answer(s)
  • Display the question to the user
  • Get the user's answer
  • Check if the user's answer was correct

But, the specifics of how each of these question types stores the correct answer (and what data type it is) and validates it differ between each of them…

  User answer Stored answer Validate
True/false bool bool answer Userinput = answer?=
MultiChoice int string options[4] Userinput = answer?=
FillIn int string answer Userinput = answer?=

We could design our Questions so that they have functionality that interacts with the user directly (e.g., a bool function that asks the user to enter their response and returns true if they got it right and false if not) rather than writing functions around returning the actual answer (which would be more difficult because they have different data types).

  • Set up question
  • Run question
Declarations:

We can set up a simple interface for our Questions with these functions. They've been marked as virtual, which allows us to use polymorphism, and they've also been marked with = 0 at the end, marking them as pure virtual - this tells the compiler that child classes must implement their own version of these methods. A function that contains pure virtual methods is called an abstract class.

class Question
{
  public:
  virtual void Setup() = 0;
  virtual bool Run() = 0;

  protected:
  string m_question;
};

Now our child classes can inherit from Question. They will be required to override Setup() and Run(), and we can also have additional functions as needed for that implementation:

class MultipleChoiceQuestion : public Question
{
  public:
  virtual void Setup();
  virtual bool Run();
  void ListAllAnswers();

  protected:
  string m_options[4];
  int m_answer;
};
Definitions:

Each class will have its own implementation of these interface functions, but since they're part of an interface, when we build a program around these classes later we can call all of them the same way.

Question:

void Question::Setup() {
    cout << "Enter question: ";
    getline( cin, m_question );
}

TrueFalseQuestion:

void TrueFalseQuestion::Setup() {
    Question::Setup();
    cout << "Enter answer (0 = false, 1 = true): ";
    cin >> m_answer;
}

MultipleChoiceQuestion:

void MultipleChoiceQuestion::Setup() {
  Question::Setup();

  for ( int i = 0; i < 4; i++ )
    {
      cout << "Enter option " << i << ": ";
      getline( cin, m_options[i] );
    }

  cout << "Which index is correct? ";
  cin >> m_answer;
}

FillInQuestion:

void FillInQuestion::Setup() {
    Question::Setup();
    cout << "Enter answer text: ";
    getline( cin, m_answer );
}
Function calls:

Now, no matter what kind of question subclass we're using, we can utilize the same interface - and the same code.

// Create the pointer
Question* ptr = nullptr;

// Allocate memory
if ( choice == "true-false" )
{
    ptr = new TrueFalseQuestion();
}
else if ( choice == "multiple-choice" )
{
    ptr = new MultipleChoiceQuestion();
}
else if ( choice == "fill-in" )
{
    ptr = new FillInQuestion();
}

// Set up the question
ptr->Setup();

// Run the question
ptr->Run();

// Free the memory
delete ptr;

And, utilizing this interface, we could then store a vector<Question*> and set up each question as any question subclass without any duplicate code.


5. Example usage: Game objects

Let's say we have created a family tree of game objects, starting at the most basic object that has an \((x, y)\) coordinate and dimensions:

class GameObject
{
public:
  GameObject();

  void Setup( int initialX = 0, int initialY = 0, const std::string& name = "unnamed" );
  void SetTexture( const sf::Texture& texture, sf::IntRect& textureCoordinates );
  std::string GetName() const;
  void Update();
  void Draw();
  // ... etc ...

protected:
  sf::Vector2f m_position;
  std::string m_name;
  sf::Sprite m_sprite;
  sf::IntRect m_textureCoordinates;
  // ... etc ...
};

Then we might have something like an unanimated item in the world, but maybe it has physics so it needs the ability to update:

class Item : public GameObject
{
public:
  Item();
  void SetObjectType( ObjectType type );
  std::string GetObjectTypeName() const;
  void SetImageCode( int code );
  int GetImageCode() const;
  const sf::Sprite& GetSprite() const;
  void Update();
  void Draw();
  // ... etc ...

protected:
  ObjectType m_objectType;
  int m_imageCode;
  // ... etc ...
};

But then our player and NPC characters also have animated sprites and the ability to move with keyboard input or rudimentary AI:

class Character : public GameObject
{
public:
    Character();

    void Update();
    void Draw();
    const sf::Sprite& GetSprite() const;

    void SetSpeed( int speed );
    void SetDirection( Direction direction );
    Direction GetDirection() const;
    CharacterType GetCharacterType() const;

    void SetAnimationInformation( int maxFrames, float animationSpeed );

    void Move( Direction direction, const Map::ReadableMap& gameMap );
    void Move( Direction direction );
    void Move( Direction direction, int minX, int minY, int maxX, int maxY );
    sf::IntRect GetDesiredPosition( Direction direction );

    GravityHandler gravity;

    void ForceSpriteUpdate();
    void Animate();

    void RestrictMovement();
    void SetRestrictMovement( bool value );
    sf::IntRect GetValidPositionRegion() const;
    void SetValidPositionRegion( sf::IntRect rect );

protected:
    void MoveClipping( Direction direction );
    void BeginAttack();
    void EndAttack();

    Direction m_direction;
    int m_speed;
    float m_animationFrame;
    float m_maxFrames;
    float m_animationSpeed;

    SheetAction m_sheetAction;
    float m_sheetActionTimer;

    Action m_stateAction;

    CharacterType m_characterType;

    sf::IntRect m_validPositionRegion;
    bool m_restrictMovement;

};

If we didn't use polymorphism, we would have to store all objects in their own vector:

vector<Character> m_npcList;
vector<Item> m_pickups;
vector<GameObject> m_decor;

But utilizing polymorphism, we can store one vector of GameObject* objects initialized on the heap, and any common functionality they have (Update, Draw, etc.) could be accessed via that pointer.

// Our storage
vector<GameObject*> m_entities;

// Creating a new item (elseware in program)
GameObject* newItem = new Item;

// Adding it to the list
m_entities.push_back( newItem );

// Accessing it later
for ( auto& entity : entities )
{
  entity->Update();
}

Author: Rachel Wil Sha Singh

Created: 2023-10-05 Thu 21:25

Validate