Your browser doesn't support JavaScript Polymorphism – Windows Programming

Polymorphism

Polymorphism refers to the ability to access different types of member functions depending on the type of object that invokes the function. Polymorphism can be static or dynamic. C++ supports compile-time polymorphism and run-time polymorphism.

With compile-time polymorphism, the compiler determines which function should be called. Function overloading and operator overloading are examples of compile-time polymorphism.

With run-time polymorphism, the function to be called is determined while the programme is running. This is commonly achieved using virtual functions and inheritance.

Compile-Time Polymorphism

This type of polymorphism involved either function or operator overloading

Function overloading

Function or method overloading is the ability to create multiple methods of the same name with different parameter implementations.  The compiler determines which function to call based on the arguments supplied.

In the example below, the first function is called when an int is supplied, the second when a char is supplied, and the third when two int values are supplied.

#include <iostream>

class Base
{
public:
    // Function with an int parameter
    void func(int x)
    {
        std::cout << "Value of x is " << x << std::endl;
    }

    // Function with a char parameter
    void func(char x)
    {
        std::cout << "Value of x is "
                  << x << " or "
                  << static_cast<int>(x) << std::endl;
    }

    // Function with two int parameters
    void func(int x, int y)
    {
        std::cout << "Value of x and y is "
                  << x << ", " << y << std::endl;
    }
};

int main()
{
    Base obj1;

    // Which function is called depends on the arguments supplied
    obj1.func(33);       // Calls the first func()
    obj1.func('!');      // Calls the second func()
    obj1.func(33, 66);   // Calls the third func()

    return 0;
}
The output is:

Value of x is 33
Value of x is ! or 33
Value of x and y is 33, 66

The three functions all have the same name, func, but their parameter lists are different. The compiler uses the arguments supplied in each function call to determine which version should be used.

The conversion from char to int in the second function is used to display the numerical character code for !.

Operator overloading

Operator overloading allows the programmer to define the behaviour of an operator for a particular class. Almost all operators in C++ can be overloaded. To create an overloaded operator, an operator function is created to define the action of the overloaded operator. The function name is preceded by the keyword “operator” and then the symbol for the operator is defined. An overloaded operator function can have a return type and a parameter list.

Operators are commonly classified according to the number of operands they use. A unary operator operates on one operand, while a binary operator operates on two operands.

Overloading the Binary Addition Operator

The following example overloads the binary addition operator (+) so that two Area objects can be added together.

#include <iostream>

class Area
{
private:
    int x;
    int y;

public:
    Area(int a = 0, int b = 0)
    {
        x = a;
        y = b;
    }

    // Overloaded + operator
    Area operator+(const Area& rhs) const
    {
        Area returnObject;

        returnObject.x = x + rhs.x;
        returnObject.y = y + rhs.y;

        return returnObject;
    }

    void Print() const
    {
        std::cout << x << " " << y << std::endl;
    }
};

int main()
{
    Area c1(10, 5);
    Area c2(2, 4);

    Area c3 = c1 + c2;

    c3.Print();

    return 0;
}

The output is: 12 9.

The expression: c1 + c2 uses the overloaded + operator to add the corresponding x and y values from the two objects. In this example, it is equivalent to calling:c1.operator+(c2);.The resulting Area object is returned and used to initialise c3.

Overloading the unary increase and decrease operator 

The increment (++) and decrement (–) operators are unary operators because they operate on a single object.

Both operators can be overloaded as either prefix or postfix operators.

With the prefix form, the operator appears before the object:

With the postfix form, the operator appears after the object:

When overloading these operators, C++ uses an int parameter to distinguish the postfix version from the prefix version. The parameter is not normally used.

The following example overloads both the increment and decrement operators:

#include <iostream>

class Overloaded
{
private:
    int count;

public:
    Overloaded() : count(1) {}

    // Prefix increment
    void operator++()
    {
        ++count;
    }

    // Postfix increment
    void operator++(int)
    {
        count++;
    }

    // Prefix decrement
    void operator--()
    {
        --count;
    }

    // Postfix decrement
    void operator--(int)
    {
        count--;
    }

    void DisplayCount() const
    {
        std::cout << "Count: " << count << std::endl;
    }
};

int main()
{
    Overloaded value;

    ++value;              // Calls prefix increment
    value.DisplayCount();

    value++;              // Calls postfix increment
    value.DisplayCount();

    --value;              // Calls prefix decrement
    value.DisplayCount();

    value--;              // Calls postfix decrement
    value.DisplayCount();

    return 0;
}

The output is:

Count: 2
Count: 3
Count: 2
Count: 1

Dynamic or Run-Time Polymorphism

In contrast, with compile time polymorphism, the compiler determines which function call to bind to the object after deducing it at runtime. This allows different objects to respond differently to the same method call. In C++, run-time polymorphism is commonly implemented using virtual functions, allowing a base-class pointer or reference to call an overridden function in a derived class

Virtual function –  is a member function declared in a base class that can be overridden by a derived class. When a derived class object is accessed through a pointer or reference to the base class, a virtual function allows the derived-class implementation to be called.

For example

#include <iostream>

using namespace std;

class Base
{
public:

    virtual void func()
    {
        cout << "Base class function call" << endl;
    }
};

class Derived : public Base
{
public:

    void func()
    {
        cout << "Derived class function call" << endl;
    }
};

int main()
{
    Base b;
    Derived d;

    b.func();       // Call Base::func()
    d.func();       // Call Derived::func()

    Base* p = &d;   // Base pointer referring to Derived object
    p->func();      // Call Derived::func() at run time
}

The first two calls are straightforward. b.func() calls Base::func(), while d.func() calls Derived::func().

The important part is:

Base* p = &d; p->func();

Although p is a pointer to Base, it refers to a Derived object. Because func() is virtual, C++ selects Derived::func() at run time.

The output is:

Base* p = &d; p->func();

Base class function call
Derived class function call
Derived class function call

The override keyword is optional. It can be used in the derived class to tell the compiler that the function is intended to override a virtual function. If it does not correctly override a base-class virtual function, the compiler reports an error.

Early and Late Binding

A non-virtual function uses early binding. The function call is determined at compile time according to the type of the pointer or reference.

A virtual function uses late binding when called through a base-class pointer or reference. The function is selected at run time according to the type of object being referred to.

For example:

class Base
{
public:

    virtual void virtfunc()
    {
        cout << "Base virtual function" << endl;
    }

    void func()
    {
        cout << "Base non-virtual function" << endl;
    }
};

class Derived : public Base
{
public:

    void virtfunc()
    {
        cout << "Derived virtual function" << endl;
    }

    void func()
    {
        cout << "Derived non-virtual function" << endl;
    }
};

If a base-class pointer refers to a derived-class object:

Base* p = &d; p->func(); p->virtfunc();

the output is:

Base non-virtual function Derived virtual function

The non-virtual func() uses early binding and calls Base::func(). The virtual virtfunc() uses late binding and calls Derived::virtfunc().

Calling the Base-Class Function

The base-class implementation of an overridden function can be explicitly selected using the scope resolution operator (::):

d.Base::func();

This calls Base::func() directly rather than using the virtual function mechanism.

The scope resolution operator is therefore used when a specific class implementation needs to be selected explicitly.

Virtual Destructors

A virtual destructor ensures that when a derived-class object is deleted through a pointer to its base class, the derived-class destructor is called before the base-class destructor.

This is important when a base-class pointer is used to refer to a derived-class object. If the base-class destructor is not virtual and the object is deleted through a base-class pointer, the behaviour is undefined.

For example:

Base* p = new Derived; delete p;

If Base has a virtual destructor, C++ first calls the Derived destructor and then the Base destructor. This ensures that the derived part of the object is properly destroyed before the base part.

A virtual destructor is therefore particularly important when a class is intended to be used as a polymorphic base class.

The following example demonstrates the difference between an object created on the free store and an object created with automatic storage duration:

#include <iostream>

using namespace std;

class Base
{
public:

    Base()
    {
        cout << "Base Constructor" << endl;
    }

    virtual ~Base()
    {
        cout << "Base Destructor" << endl;
    }
};

class Derived : public Base
{
public:

    Derived()
    {
        cout << "Derived Constructor" << endl;
    }

    ~Derived()
    {
        cout << "Derived Destructor" << endl;
    }
};

void DeleteMemory(Base* pBase)
{
    delete pBase;
}

int main()
{
    cout << "Allocating a derived object on the free store:" << endl;

    Derived* pDerived = new Derived;

    cout << "Deleting derived class:" << endl;

    DeleteMemory(pDerived);

    cout << "Instantiating a derived class on the stack:" << endl;

    Derived drv;

    cout << "Automatic destruction as it goes out of scope:" << endl;

    return 0;
}

When the Derived object is created with new, the base-class constructor is called first, followed by the derived-class constructor.

When the object is deleted through the base-class pointer, the virtual destructor causes the destruction to take place in the reverse order:

Derived Destructor Base Destructor

The Derived destructor therefore runs before the Base destructor.

The object drv is different because it has automatic storage duration. It is automatically destroyed when it reaches the end of main(). Its destructors are also called in the correct order:

Derived Destructor Base Destructor

The important point is that inheritance already defines the normal construction and destruction order. The purpose of making the base destructor virtual is to ensure that this destruction sequence is also used when a derived object is deleted through a base-class pointer.

Pure Virtual Functions and Abstract Classes

An abstract class is a class that contains at least one pure virtual function. An abstract class cannot be instantiated directly. It is normally used as a base class that defines an interface or common functionality for derived classes.

virtual void pvf() = 0;

A pure virtual function is declared by placing = 0 after its declaration:

A class containing a pure virtual function is an abstract class. Any derived class must provide an implementation of all inherited pure virtual functions in order to become a concrete class that can be instantiated. If it does not, the derived class remains abstract.

A pure virtual function does not normally have a function body in the base-class declaration. However, C++ does allow a pure virtual function to have a separate definition outside the class. The = 0 still makes the function pure virtual.

#include <iostream>

using namespace std;

class Base
{
public:

    virtual void pvf() = 0;  // Pure virtual function

    Base()
    {
        cout << "Base constructor called" << endl;
    }
};

class Derived : public Base
{
public:

    Derived()
    {
        cout << "Derived constructor called" << endl;
    }

    void pvf() override
    {
        cout << "Derived implementation of pvf()" << endl;
    }
};

int main()
{
    Derived d;

    d.pvf();

    return 0;
}

The Base class is abstract because it contains the pure virtual function pvf(). Therefore, the following would produce a compilation error:

Base b;

However, a Derived object can be created because Derived provides an implementation of pvf().

The output is:

Base constructor called
Derived constructor called
Derived implementation of pvf()

An abstract class can also be used through pointers and references:

Base* p = new Derived; p->pvf(); delete p;

Here, p is a pointer to Base, but it refers to a Derived object. Because pvf() is virtual, the Derived implementation is called.

Characteristics of an Abstract Class

  • An abstract class cannot be instantiated directly, but pointers and references to an abstract class can be created.
  • An abstract class can contain normal member functions as well as pure virtual functions.
  • Abstract classes are commonly used to define an interface and provide common functionality for derived classes.
  • A derived class must implement all inherited pure virtual functions before it can be instantiated. If it does not, the derived class remains abstract.
  • A pure virtual function is declared by using = 0 after the function declaration.