Categories
Computer Science / Information Technology Language: C++

Polymorphism

Overview

In this section, we will explore how polymorphism and inheritance can empower us to develop reusable and adaptable programs. Our journey begins with an examination of the various types of polymorphism available in C++. Specifically, we will focus on dynamic polymorphism.

Next, we will delve into the utilisation of base class pointers, which will elevate our class hierarchies to new heights. By employing this technique, we can embrace abstract thinking and alleviate concerns over intricate details.

Throughout this course section, we will encounter examples that illustrate both static and dynamic binding of function calls. By analysing their advantages and disadvantages, we will gain a comprehensive understanding of each approach.

Subsequently, we will leverage virtual functions to establish polymorphic functions, allowing for runtime binding. Additionally, we will explore the significance of virtual destructors and utilise the C++11 override and final specifiers.

To further harness dynamic polymorphism, we will explore the use of base class references. This technique offers additional flexibility and adaptability in our programs.

Finally, we will uncover the concepts of pure virtual functions and abstract classes, highlighting their importance in both standalone implementations and interfaces.

Introduction to polymorphism

Polymorphism, in simple terms, refers to the ability of an object to take on many forms. It allows you to treat different objects in a similar way, even if they belong to different classes. Think of polymorphism as a way of interacting with objects without knowing their specific types. You can perform actions on objects based on their common characteristics or behaviours, rather than their specific implementations.

For example, imagine you have different shapes, such as a square, a circle, and a triangle. Each shape has its own unique properties, but they all share a common characteristic of being able to calculate their area. Polymorphism allows you to write a piece of code that can calculate the area of any shape, regardless of its specific type. You can treat each shape object as a “shape” and call a common function like calculateArea(), which is implemented differently in each shape class.

By leveraging polymorphism, you can write generic code that works with a variety of objects without explicitly knowing their individual details. This flexibility makes the code more adaptable, maintainable, and reusable.

Types of polymorphism

There are two main types of polymorphism: compile-time polymorphism and runtime polymorphism.

Compile-time Polymorphism

Compile-time polymorphism (also known as static binding or early binding polymorphism) is determined during the compilation phase of a program. It involves the use of function overloading and operator overloading (discussed in earlier sections). The specific function or operator to be executed is resolved by the compiler based on the arguments provided at compile-time.

Runtime Polymorphism

Runtime polymorphism (also known as dynamic binding/late binding polymorphism) is determined during the runtime or execution phase of a program. It involves the use of inheritance and virtual functions. As discussed in the previous section, Inheritance allows a derived class to inherit properties and behaviours from a base class.

Virtual functions are functions declared in a base class and overridden in derived classes. When a base class pointer or reference is used to refer to an object of a derived class, the virtual function call is resolved at runtime, and the appropriate derived class implementation is executed. This allows different objects of derived classes to be treated as objects of the base class, and the appropriate functions are called dynamically based on the actual object type.

We shall delve into virtual function in greater detail in the upcoming sections.

Contrast between compile time and runtime polymorphism

First, let’s examine a non-polymorphic example that utilises static binding. In the scenario, we have an account hierarchy implemented using public inheritance, as depicted in the following figure.

Each account class has its own unique version of the withdraw method, which varies depending on the account type. We’ll create four objects, one for each account type, and invoke their respective withdraw methods.

Account a;
a.withdraw(1000); // Account::withdraw()

Savings b;
b.withdraw(1000); // Savings::withdraw()

Checking c;
c.withdraw(1000); // Checking::withdraw()

Trust d;
d.withdraw(1000); // Trust::withdraw()

When we invoke the withdraw method of object ‘a’, it calls the withdraw method of the account class. This is logical since ‘a’ is an account object. The compiler is aware of this and binds the method call during compile-time or statically. The same principle applies to objects ‘b’, ‘c’, and ‘d’. In each case, the compiler binds the withdraw calls based on the object information provided when they were declared in the source code. Thus, ‘b’ invokes the withdraw method of the savings class, ‘c’ invokes the withdraw method of the checking class, and ‘d’ invokes the withdraw method of the trust class. So far, everything aligns with our expectations. Now consider the following snippet:

Account *p = new Trust();
p->withdraw(1000);

p is a pointer to an account object, holding the address of an account. Now, we dynamically create a trust account object on the heap and assign its address to p. Is this permissible? Indeed, it is. p can hold addresses of accounts, and trust is an account according to the inheritance hierarchy.

But what happens when we invoke the withdraw method on the object pointed to by p? Since static binding is the default behaviour, the compiler doesn’t possess knowledge of the account type that p is pointing to during runtime. It simply doesn’t concern itself with it. All it knows is that p is pointing to an account. As a result, it calls the withdraw method of the account class. This outcome is likely not what we expected or desired since we want the trust object on the heap to employ its own version of the withdraw method. All the compiler knows is that p is a pointer to an account so it will bind the withdraw method to the account class’s withdraw method at compile time.

Let’s consider another example where we have the same class hierarchy as before. Let’s assume that each account class possesses its own implementation of a display method, which is responsible for showcasing account information based on the account type.

To illustrate this, we can create a simple C++ function called displayAccount. This function expects a reference to an account object as input. Since all the derived classes are considered accounts, we can pass any of them into this function. Consequently, the function will invoke the appropriate display method based on the account object provided.

Account a;
displayAccount(a);

Savings b;
displayAccount(b);

Checking c;
displayAccount(c);

Trust d;
displayAccount(d);

void displayAccount(const Account &acc) {
    acc.display();
    // will always use Account::display
}

The code snippet demonstrates the creation of four objects: a, b, c, and d, each representing a different account type. These objects are then passed to the displayAccount function. However, the behaviour observed from the displayAccount function may not align with initial expectations.

By default, C++ adheres to static binding, which means that when the compiler encounters the call to acc.display within the displayAccount function, it binds the call to the display method of the account class. As a result, the account’s display method will be invoked regardless of the object passed in. Consequently, the display will only showcase the contents of the account section.

It is possible for C++ to inquire about the type of account object being passed in by asking, “Hey, what kind of account are you?” and based on the response, if-else statements could be employed to invoke the appropriate display methods. However, relying on such conditional statements is considered poor coding practice as it hampers program abstraction. In such cases, we would need to determine the object’s type explicitly and call its functions accordingly.

Using a Base Class Pointer

We have already learned that in C++, dynamic binding of method calls requires an inheritance hierarchy, the use of base class pointers or references, and the declaration of the desired methods as virtual. In this section, we will focus on the importance and power of using base class pointers. Let’s consider the following account class hierarchy:

First, let’s assume that this class hierarchy now utilises dynamic polymorphism. We will explore how to achieve this later in the section. For now, let’s concentrate on its effects and the advantages it offers. We will create four pointers to account objects and initialise each one with a different type of account created on the heap.

Account* p1 = new Account();
Account* p2 = new SavingsAccount();
Account* p3 = new CheckingAccount();
Account* p4 = new TrustAccount();

This is valid because all these objects are accounts due to the “is-a” relationship established through public inheritance. Next, using these base class pointers, we can call the withdraw method, and C++ will determine which method to bind at runtime based on the type of the object being pointed to by each pointer.

p1->withdraw(1000);   // Account::withdraw
p2->withdraw(1000);   // SavingsAccount::withdraw
p3->withdraw(1000);   // CheckingAccount::withdraw
p4->withdraw(1000);   // TrustAccount::withdraw

// delete the pointers

This dynamic binding of method calls is incredibly powerful. Once the hierarchy is set up, we don’t need to do anything else. However, it’s essential to deallocate the memory allocated by the pointers when we are finished using them.

Now, let’s explore a more compelling use case for base class pointers. In the following example, we have the same four pointers as before, but this time, I’ve declared an array that holds pointers to account objects.

Account* accountArray[] = { p1, p2, p3, p4 };

Here, the elements of the array are base class pointers. We can loop through the array and call the withdraw method for each element. The correct withdraw method will be called based on the type of the object each pointer points to.

for (int i = 0; i < 4; ++i) {
    accountArray[i]->withdraw();
}

This demonstrates the power of using base class pointers. It doesn’t matter how many pointers we initialise the array with or what types of accounts they point to; it will work as expected. Even if we replace an array element with another, it will still work correctly. This is programming more abstractly or more generally. We can simply think of calling the withdraw method for each account in the array without worrying about the specific details. The same concept applies to other collections such as vectors.

std::vector<Account*> accountVector { p1, p2, p3, p4 };

for (auto account : accountVector) {
    account->withdraw();
}

Consider what would happen if we added another class to our account hierarchy, like a bond account. None of the existing code that works with account objects would need to be modified. Since a bond account is an account, it will seamlessly work with our existing code, allowing us to leverage the benefits of polymorphism and abstraction.

Virtual functions

Let’s delve into the concept of virtual functions and the usage of the virtual keyword to achieve dynamic binding in class hierarchies. When we derive a class from a base class, we have the ability to redefine the behaviour of base class functions in the derived class, creating specialised versions of those functions. By default, these overridden functions are statically bound at compile time. However, by using the virtual keyword, we can make them dynamically bound, enabling runtime binding based on the actual type of the object.

To declare a function as virtual, we use the virtual keyword in the base class. Let’s take a look at the syntax. In the code snippet below, we have the account base class, and we declare the withdraw method as virtual:

class Account {
public:
    virtual void withdraw(double amount) {
        // Base implementation
    }
};

By marking the withdraw function as virtual, we indicate that it can be overridden in derived classes. This enables us to treat objects in the class hierarchy abstractly, using base class pointers or references. Once a function is declared as virtual in the base class, it remains virtual throughout the derived classes.

Let’s consider an example where we override the withdraw function in a derived class called Checking:

class Checking : public Account {
public:
    void withdraw(double amount) {
        // Derived class implementation
    }
};

In this example, the withdraw function in the Checking class is implicitly virtual, thanks to the virtual declaration in the base class. However, it’s good practice to explicitly use the override keyword to make it clear that we are overriding a base class function. This makes the compiler throw an error if there is any deviation in the function signatures of the parent class virtual function from the derived class virtual function. The syntax to do so is as follows:

class Checking : public Account {
public:
    void withdraw(double amount) override {
        // Derived class implementation
    }
};

It’s important to note that when overriding a base class function, the function signature and return type must match exactly. Any deviation will be treated as a redefinition rather than an override, resulting in static binding.

Remember, virtual functions are dynamically bound only when they are called through a base class pointer or reference. If called directly through an object, they are statically bound.

Let’s consider an elaborate example using the account class hierarchy to demonstrate dynamic polymorphism, base class pointers, and the power of abstraction. We will create a banking system that manages different types of accounts: regular accounts, savings accounts, checking accounts, and trust accounts.

First, let’s define the base class Account:

#include <iostream>

class Account {
protected:
    double balance;

public:
    Account(double balance = 0.0) : balance(balance) {}

    virtual void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
            std::cout << "Withdrew $" << amount << " from Account. New balance: $" << balance << std::endl;
        } else {
            std::cout << "Insufficient funds in Account." << std::endl;
        }
    }
};

Next, we’ll create derived classes: SavingsAccount, CheckingAccount, and TrustAccount. Each of these classes will provide its own implementation of the withdraw method:

class SavingsAccount : public Account {
public:
    SavingsAccount(double balance = 0.0) : Account(balance) {}

    void withdraw(double amount) override {
        // Additional logic specific to SavingsAccount
        std::cout << "Withdrawing $" << amount << " from Savings Account." << std::endl;
        Account::withdraw(amount);
    }
};

class CheckingAccount : public Account {
public:
    CheckingAccount(double balance = 0.0) : Account(balance) {}

    void withdraw(double amount) override {
        // Additional logic specific to CheckingAccount
        std::cout << "Withdrawing $" << amount << " from Checking Account." << std::endl;
        Account::withdraw(amount);
    }
};

class TrustAccount : public Account {
public:
    TrustAccount(double balance = 0.0) : Account(balance) {}

    void withdraw(double amount) override {
        // Additional logic specific to TrustAccount
        std::cout << "Withdrawing $" << amount << " from Trust Account." << std::endl;
        Account::withdraw(amount);
    }
};

Now, let’s simulate the banking system. We’ll create instances of different account types and demonstrate dynamic polymorphism by using base class pointers:

int main() {
    // Create account objects
    Account* account = new Account(1000.0);
    Account* savingsAccount = new SavingsAccount(2000.0);
    Account* checkingAccount = new CheckingAccount(1500.0);
    Account* trustAccount = new TrustAccount(5000.0);

    // Call withdraw method using base class pointers
    account->withdraw(200.0);
    savingsAccount->withdraw(500.0);
    checkingAccount->withdraw(1000.0);
    trustAccount->withdraw(3000.0);

    // Clean up memory
    delete account;
    delete savingsAccount;
    delete checkingAccount;
    delete trustAccount;

    return 0;
}

When we run this program, we’ll see the appropriate logging messages based on the type of account and the amount being withdrawn. The dynamic binding will ensure that the correct version of the withdraw method is called for each account type.

Output:

Withdrew $200 from Account. New balance: $800
Withdrawing $500 from Savings Account.
Withdrew $500 from Account. New balance: $1500
Withdrawing $1000 from Checking Account.
Insufficient funds in Account.
Withdrawing $3000 from Trust Account.
Withdrew $3000 from Account. New balance: $2000

This example demonstrates the use of dynamic polymorphism, base class pointers, and the power of abstraction. We can handle different account types through a common base class interface, allowing us to write code that works with accounts in a general and abstract manner, without needing to know the specific account types involved.

Virtual destructors

Virtual destructors are an essential aspect when working with polymorphic objects and inheritance hierarchies in C++. In the previous example, you might have encountered warning messages during compilation regarding the absence of a virtual destructor in our simple account class hierarchy.

warning: deleting object of polymorphic class type 'Account' which has non-virtual destructor might cause undefined behaviour

These warnings indicate that deleting a polymorphic object without a virtual destructor can lead to unpredictable behaviour. According to the C++ standard, if a derived class object is destroyed through a base class pointer and the base class does not have a virtual destructor, the behaviour is undefined.

To address these warnings and ensure proper destruction of derived objects, we need to provide a virtual destructor in the base class. This ensures that the destructors of both the base and derived classes are called in the correct order. Declaring a destructor as virtual is straightforward and intuitive. Once the base class has a virtual destructor, all derived classes automatically have virtual destructors as well, without the need to explicitly declare them as virtual (although it’s considered best practice to do so).

Here’s an example of the code snippet for the account base class, showcasing the addition of a virtual destructor:

class Account {
public:
  // Other member functions

  virtual ~Account() {
    // Destructor implementation
  }
};

In the code snippet above, we have added the virtual keyword before the destructor declaration in the base class. This ensures that when an object of a derived class is deleted through a base class pointer, the appropriate destructor for the derived class will be called, followed by the destructor of the base class.

Contrast between overriding virtual functions and overriding normal functions

Inheritance and Polymorphism:

  • Virtual functions are used in the context of inheritance and polymorphism, where a derived class can override the implementation of a virtual function inherited from a base class. This enables objects of different derived classes to be treated uniformly as objects of the base class, while still invoking their specific implementations.
  • Normal functions are not involved in inheritance and polymorphism. They are standalone functions defined in a class or globally, and their behaviour remains the same regardless of any derived classes.

Function Signature:

  • When overriding a virtual function, the derived class must have the exact same function signature (i.e., return type, name, and parameters) as the base class’s virtual function. If the function signatures don’t match, it will be considered a different function and won’t override the base class function.
  • Normal functions can have different function signatures, as they are not intended to be overridden. It is possible to have multiple functions with the same name but different parameters within a class.

Binding:

  • Virtual functions use dynamic binding, also known as late binding or runtime polymorphism. The appropriate function to be executed is determined at runtime based on the actual type of the object.
  • Normal functions use static binding, also known as early binding or compile-time polymorphism. The function to be executed is determined at compile-time based on the declared type of the object or the function call.

Keyword Usage:

  • Virtual functions are declared using the virtual keyword in the base class. This keyword indicates that the function can be overridden by derived classes.
  • Normal functions are not explicitly declared with any special keyword. They are defined as regular member functions or global functions without any specific indication of being overridden.

Base Class Function Execution:

  • When a virtual function is overridden in a derived class and called through a base class pointer or reference, the overridden implementation in the derived class is executed.
  • When calling a normal function, the implementation in the class where the function is defined is executed, irrespective of the derived classes.

The C++11 final specifier

The C++11 final specifier provides us with the ability to prevent derivation and subclassing of classes, as well as overriding methods in derived classes. This specifier can be used in two different contexts.

Context of classes

When used at the class level, the final specifier prevents a class from being derived from or subclassed. This is useful in scenarios where we want to create final classes that should not be extended. It can be done to improve compiler optimization or ensure object copying safety without the risk of object slicing. The syntax for using the final specifier at the class level is straightforward. We simply add the final specifier after the class name during declaration. For example:

class MyClass final {
    // Class definition
};

class YoutClass final {
    // Class definition
};

In the above examples, both MyClass and YourClass classes are marked as final, indicating that they cannot be derived from. If an attempt is made to derive a class from these final classes, the compiler will generate an error, preventing subclassing.

Context of methods

Additionally, the final specifier can be used at the method or function level to prevent a method from being overridden in derived classes. This can be done to improve compiler optimization or to enforce a specific behaviour that should not be modified further down in the inheritance hierarchy. Let’s consider a simple class hierarchy with classes A, B, and C. Class A declares the virtual function doSomething(), and class B, derived from A, overrides it and marks it as final. Here’s an example:

class A {
public:
    virtual void doSomething() {
        // Base class implementation
    }
};

class B : public A {
public:
    void doSomething() final {
        // Derived class implementation, marked as final
    }
};

class C : public B {
public:
    // Error: Attempting to override a final function
    void doSomething() {
        // Derived class implementation
    }
};

In this example, the doSomething() function in class B is overridden and marked as final, indicating that it cannot be further overridden in any derived classes. If class C attempts to override doSomething(), the compiler will generate an error, indicating that the function is final and cannot be overridden.

Leave a Reply

Your email address will not be published. Required fields are marked *

You cannot copy content of this page