Part E - Polymorphism

Abstract Base Classes

Define a pure virtual function
Define an abstract base class
Demonstrate a unit test of an interface

"The abstract base class remains the only language construct for generating abstract types in C++."
(Wikipedia, 2007).

Pure Virtual Function | Abstract Base Class | Unit Testing | Summary | Exercises


A class that contains all of the information that the compiler needs to create an instance of the class is called a concrete class.  A class that has some of that information missin is called an abstract class.  We can only implement an abstract class by deriving from it a class that defines the missing information.  Hence, an abstract class is necessarily a base class.  Although an abstract base class cannot be implemented without some derivation, it serves the purpose of exposing select member functions of a class hierarchy.  A client programmer can code an application using the abstract base class alone and leave the implementation details to another programmer. 

This chapter describes the principal components of abstract base classes, which are called pure virtual functions, and shows how to define an abstract base class using their signatures.  The chapter concludes with an example of a unit test on an abstract base class. 


Pure Virtual Functions

The principal component of an abstract base class is a pure virtual member function.  The function is called pure due to its lack of implementation details.  The only information specified is its signature.  This signature is shared by every definition of the member function throughout the hierarchy and describes completely the interface to the hierarchy for any client programmer.  That is, a client only requires this signature to access whatever implementation may eventually be developed. 

Declaration

The declaration of a pure virtual function takes the form

 virtual Type identifier(parameters) = 0;

The assignment to 0 identifies the function as pure.  A pure function must be a virtual member function.

For example, let us specify that all concrete classes within the Person hierarchy must have some implementation of the display() const member function.  The pure virtual function that stipulates this requirement is:

 // A Pure Virtual Function
 virtual void display() const = 0;


Abstract Base Classes

An abstract base class is a class that includes or inherits at least one pure virtual function that has not been defined.  Attempts to create an instance of an abstract base class always generate a compiler error. 

Definition

An abstract base class is a set of pure virtual member functions.  The class definition contains their declarations.  An abstract base class with no data members is called an interface

For example, let us create an abstract base class named iPerson for our Person hierarchy.  To expose the display() member function to client applications, we declare it as pure.  The iPerson.h header file contains the definition of our abstract class:

 // Abstract Base Class for the Person Hierarchy 
 // iPerson.h

 #include <iostream>

 class iPerson {
   public:
     virtual void display(std::ostream&) const; 
 };

We derive our Person class from this interface.  The header file for our Person and Student class definitions includes the definition of our abstract base class:

 // Student.h

 #include "iPerson.h"
 const int N = 30;
 const int M = 13;

 class Person : public iPerson {
     char person[N+1];
   public:
     Person();
     Person(const char*);
     void display(std::ostream&) const; 
 };

 class Student : public Person {
     int no;
     char grade[M+1];
   public:
     Student();
     Student(const char*, int, const char*); 
     void display(std::ostream&) const;
 };

Declaring display() a member function of both Person and Student informs the compiler that the function will be implemented in each class and makes each class concrete. 


Unit Tests on an Interface

A recommended design for unit tests is to code them for an interface rather than an implementation.  This approach is preferred as long as the interface remains unchanged as classes are implemented or derived.  Unit tests on an interface can be performed at every upgrade throughout the lifecycle of an object without altering the test module. 

Sorter Classes

For example, consider an interface that exposes the sort() member function of a hierarchy of Sorter classes.  The Sorter module contains all of the implemented algorithms.  With every upgrade to the module, we can rerun the test suite for the interface without introducing changes to the tester module. 

The header file for our Sorter module contains:

 // iSorter.h

 class iSorter {
   public:
     virtual void sort(float*, int) = 0;
 };

 class SelectionSorter : public iSorter { 
   public:
     void sort(float*, int);
 };

 class BubbleSorter : public iSorter {
   public:
     void sort(float*, int);
 };

The header file for the Tester module contains:

 // Tester.h

 class iSorter;

 void test(iSorter*, float*, int, const char*); 

The implementation file for the Tester module contains:

 // Tester for Sorter Interface
 // tester.cpp

 #include <iostream>
 #include "iSorter.h"

 void test(iSorter* sorter, float* a, int n, const char* msg) { 
     sorter->sort(a, n);
     bool sorted = true;
     for (int i = 0; i < n - 1; i++)
         if (a[i] > a[i+1]) sorted = false;
     if (sorted)
         std::cout << msg << " is sorted" << std::endl;
     else
         std::cout << msg << " is not sorted" << std::endl;
 }

The implementation file for the Sorter module defines the sort() member function for the SelectionSorter and BubbleSorter classes:

 // Sorter.cpp

 #include "iSorter.h"

 void SelectionSorter::sort(float* a, int n) { 
     int i, j, max;
     float temp;

     for (i = 0; i < n - 1; i++) {
         max = i;
         for (j = i + 1; j < n; j++)
             if (a[max] > a[j])
                 max = j;
         temp = a[max];
         a[max] = a[i];
         a[i] = temp;
     }
 }

 void BubbleSorter::sort(float* a, int n) {
     int i, j;
     float temp;

     for (i = n - 1; i > 0; i--) {
         for (j = 0; j < i; j++) {
             if (a[j] > a[j+1]) {
                 temp = a[j];
                 a[j] = a[j+1];
                 a[j+1] = temp;
             }
         }
     }
 }

For this Sorter implementation, the following test main produces the results shown on the right:

 // Test Main for Sorter Interface
 // test_main.cpp

 #include <iostream>
 #include <ctime>
 #include "Tester.h"
 #include "iSorter.h"

 void populate(float* a, int n) {
     srand(time(nullptr));
     float f = 1.0f / RAND_MAX;
     for (int i = 0; i < n; i++)
         a[i] = rand() * f;
 }

 int main() {
     int n;
     std::cout << "Enter no of elements : ";
     std::cin >> n;
     float* array = new float[n];

     iSorter* sorter = nullptr;

     sorter = new SelectionSorter();
     populate(array, n);
     test(sorter, array, n, "SelectionSort");
     delete sorter;

     sorter = new BubbleSorter();
     populate(array, n);
     test(sorter, array, n, "BubbleSort"); 
     delete sorter;

     delete [] array;
 }

















 Enter no of elements : 1000 







 SelectionSort is sorted




 BubbleSort is sorted



 


Summary

  • a pure virtual function has a signature but no implementation
  • an abstract base class includes or inherits at least one pure virtual function that has not been implemented
  • an interface is an abstract base class with no data members
  • unit tests on an interface are preferred over tests on an implementation wherever possible

Exercises




Previous Reading  Previous: Virtual Functions Next: Function Templates   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License