Toggle Menu

Polymorphism


Virtual Functions

Polymorphism is the ability to associate multiple meanings to one function name with virtual functions and late binding. Virtual functions provide the capability of polymorphism and it is where a function is used before it is defined. The compiler waits till it is used in the program and gets the object's implementation. This technique is known as late binding or dynamic binding. Virtual functions are used when we dont know exactly how a function is implemented. This is different from Java where all functions are automatically virtual.
To declare a function as virtual, we prepend the virtual keyword: virtual Return_Type foo();

Inheritance

Inherited classes will keep the virtual properties of virtual functions in the base class (even without stating the virtual keyword).

Override vs. Redefine

  • if a virtual function is changed, it is overriden (defined at runtime)
  • if a non-virtual function is changed, it is redefined (defined at compiletime)

Disadvantages of Virtual Functions

  1. More overhead
  2. Uses more storage
  3. Late binding makes the program runs slower

override Keyword

The override keyword is used to make clear if a function is overridden or redefined. This is specified in function declaration and ensures that the function is virtual and that it is overriding a virtual base class function.
class Sale {
  virtual double bill() const;
};
class DiscountSale : public Sale {
  double bill() const override;
};
Having virtual functions allows us to accomodate multiple types under one abstraction (polymorphism).
class Book {
  string title, author;
protected:
  int numPages;
public:
  Book();
  virtual bool isHeavy() const {
    return numPages > 200;
  }
};
class Comic : public Book {
  bool isHeavy() const override {
    return numPages > 30;
  }
}

Comic c{40};
Book *pB{&c};
Book &rB{c};
Comic *pc{&c};

cout  << pc->isHeavy() // true
      << pB->isHeavy() // true
      << rb.isHeavy(); // true

Book *myBook[20];
for (int i = 0; i < 20; i++) {
  cout << myBooks[i]->isHeavy() << endl;
}

final Keyword

The final keyword prevents a virtual function from being overriden. This is useful for inherited classes that inherit virtual functions where we don't want its derived classes to override it.
class Sale {
  virtual double bill() const final;
};
class DiscountSale : public Sale {
  double bill() const;  // compiler error
};

Pure Virtual Functions

Pure virtual functions are used when a base class does not have a definition for them. Its sole purpose is for its derived classes to override and provide their own specific implementation. To make a virtual function pure, we append = 0 to its declaration. i.e. virtual type foo() = 0;

Abstract Classes

Abstract classes are classes with one or more virtual functions. Here are some properties of abstract classes:
  • can only be used as a base class
  • cannot create objects of an abstract class
  • can still write code with parameters of the abstract class type which will apply to all object of derived classes
  • inherited classes from abstract class will be abstract unless you provide definitions for all the inherited pure virtual functions

Slicing Problem

We have learnt in Inheritance that a derived class can be assigned to objects of the base class. However, this means that we can only assign values included in the parent type, and we lose any new fields that the derived class has. This is the slicing problem. Example:
class Pet {
  public:
    string name;
    virtual void print( ) const;
};
class Dog : public Pet {
  public:
    string breed;
    void print( ) const; // virtual
};
Dog vdog;
Pet vpet;
vdog.name = "Tiny";
vdog.breed = "Great Dane";
vpet = vdog;
When we assign vdog to vpet, we lose the breed field. To solve this, we use pointers and dynamic variables, and in doing so, we can treat object of the derived class as an object of the base class without losing variables.
Pet *ppet;
Dog *pdog;
pdog = new Dog;
pdog->name = "Tiny";
pdog->breed = "Great Dane";
ppet = pdog;
cout << ppet->breed; // ERROR
ppet.print();  // accesses breed field

Virtual Destructors

Consider the following example:
Base *pBase = new Derived;
delete pBase;
This calls the base class' destructor and not the derived class'. To fix this, we make the destructor virtual. It is thus good practice to make all destructors virtual.

Upcasting & Downcasting

Upcasting

Upcasting is casting from a descendent object to an anscestor type. This is safe s you're only disregarding some info.

Downcasting

Downcasting is casting from an ancestor object to a descendent type. This is dangerous as we are not assumming that additional member fields are being added. To solve this, we use dynamic_cast which works only with pointer types.
Pet *ppet;
ppet = new Dog;
Dog *pdog = dynamic_cast(ppet);
dynamic_cast informs us if it fails by returning NULL. Keep the following points in mind when downcasting:
  1. Keep track of member fields so you know the info to be added is present
  2. Member functions must be virtual since dynamic_cast uses the virtual functions info to perform cast

Late Binding Implementation

C++ implements late binding by creating a virtual function table. This is created for a class that has one or more member functions that are virtual. It has a pointer for each virtual member function. If the inherited virtual function is not changed, the table points to the definition in the ancestor class. If the virtual function has a new definition, the pointer points to that definition. A vptr is placed in each class with virtual functions which points to the vtable of that object.
When a virtual function call is made through a base class pointer, the compiler inserts code to fetch the vptr. When there is no virtual function, the size of the object is equal to the size of its members. If there is one or more virtual functions, the object's size includes an extra size of a void pointer (points to vtable). Once the object with virtual functions is created, the vptr is initailized to point to the starting address of the vtable.

Steps In Calling Virtual Method

  1. Follow vptr to the vtable
  2. Fetch the pointer to the actual method from the vtable
  3. Follow the function pointer and call the function