Part E - Polymorphism

Templates

Design polymorphic objects to amplify the reusability of code
Introduce function and class templates
Introduce constrained casting to improve type safety

"Templates are of great utility to programmers in C++, especially when combined with multiple inheritance and operator overloading. " Wikipedia (2013).

Function Templates | Class Templates | Constrained Casts | Summary | Exercises



Polymorphism is not restricted to related types in object-oriented languages.  Many languages also support selection across unrelated types.  This polymorphism, which perfects the separation of interfaces from implementations, is called parametric or generic polymorphism.  In parametric polymorphism the type and the logic executed on that type are independent of one another.  Different clients can access the same logic using different totally unrelated types. 

The C++ language implements parametric polymorphism using template syntax.  The compiler generates the implementation for each client type at compile-time from the template defined by the developer. 

This chapter describes how to implement parametric polymorphism using template syntax with reference to functions and classes.  This chapter also describes the templated keywords available for casting values from one type to another. 


Function Template

Template Syntax

A template definition resembles that of a global function with the parentheses replaced by angle brackets.  A template header takes the form

 template<Type identifier[, ...]>

The keyword template identifies the subsequent code block as a template.  The less-than greater-than angle bracket pair (< >) encloses the parameter definitions for the template.  The ellipsis denotes more comma-separated parameters.  identifier is a placeholder for the argument specified by the client. 

Each parameter declaration consists of a type and an identifier.  Type may be any of

  • typename - to identify a type (fundamental or compound)
  • class - to identify a type (fundamental or compound)
  • int, long, short, char - to identify a non-floating-point fundamental type
  • a template parameter

The following examples are equivalent to one another:

 template <typename T>

 // ... template body follows here

     T value; // value is of type T 

 template <class T>

 // ... template body follows here

     T value; // value is of type T 

The compiler replaces T with the argument specified by the client code.

Complete Definition

Consider the following function that swaps values in two different memory locations.  This code is defined using references to two int variables:

 void swap(int& a, int& b) {
     int c;
     c = a;
     a = b;
     b = c;
 }

The template for all functions that swap values in this way follows from replacing the specific type int with the type variable T and inserting the template header:

 // Template for swap
 // swap.h

 template<typename T>
 void swap(T& a, T& b) {
     T c;
     c = a;
     a = b;
     b = c;
 }

We place template definitions in header files; in this case, in swap.h

Calling a Templated Function

A call to a templated function determines the specialization that the compiler generates.  The compiler binds the call to that specialization. 

For example, to call the swap() function for two doubles and two longs, we write the following and leave the remaining work to the compiler:

 // Calling a Templated Function
 // swap.cpp

 #include <iostream>
 #include "swap.h" // template definition 

 int main() { 
     double a = 2.3;
     double b = 4.5;
     long   d = 78;
     long   e = 567;

     swap(a, b); // compiler generates
                 // swap(double, double)

     std::cout << "Swapped values are " << 
          a << " and " << b << std::endl; 

     swap(d, e); // compiler generates
                 // swap(long, long)

     std::cout << "Swapped values are " <<
          d << " and " << e << std::endl; 
 }















 Swapped values are 4.5 and 2.3 





 Swapped values are 567 and 78

 

If the arguments in each call are unambiguous in their type, the compiler can specialize the template appropriately.  If the arguments are ambiguous, the compiler reports an error.


Class Template

The syntax for class templates is similar to that for function templates. 

The following template defines Array classes of specified size in static memory.  The template parameters are the type (T) of each element in the array and the number of elements in the array (N):

 // Template for Array Classes
 // Array.h

 template <class T, int N>
 class Array {
     T a[N];
 public:
     T& operator[](int i) { return a[i]; } 
 };

For the following code, the compiler generates the class definition for an array of element type int and size 5 from the Array template definition.  The output from executing this client program is shown on the right:

 // Class Template
 // Template.cpp

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

 int main() {
     Array<int, 5> a, b;

     for (int i = 0; i < 5; i++)
         a[i] = i * i;

     b = a;

     for (int i = 0; i < 5; i++)
         std::cout << b[i] << ' ';
     std::cout << std::endl;
}















 0 1 4 9 16



Constrained Casts

Constrained casts improve type safety.  Type safety is an important feature of any strongly typed language.  Bypassing the type system introduces ambiguity to the language itself and is best avoided.  Casting a value from one type to another type circumvents the type system's type checking facilities.  It is good programming practice to implement casts only where absolutely unavoidable and localize them as much as possible. 

C++ supports constrained type casting through template syntax using one of the following keywords:

  • static_cast<Type>(expression)
  • reinterpret_cast<Type>(expression)
  • const_cast<Type>(expression)
  • dynamic_cast<Type>(expression)

Type specifies the destination type.  expression refers to the value to be cast to the destination type.

Related Types

The static_cast<Type>(expression) keyword converts the expression from its evaluated type to the specified type.  By far, this is the most common form of constrained cast.

For example, to cast minutes to a float type, we write:

 // Cast to a Related Type
 // static_cast.cpp

 #include <iostream>

 int main() {
     double hours;
     int minutes;

     std::cout << "Enter minutes : ";
     std::cin >> minutes;
     hours = static_cast<double>(minutes)/ 60;  // int and float are related 
     std::cout << "In hours, this is " << hours;
 }

static_cast<Type>(expression) performs limited type checking.  It rejects conversions between pointer and non-pointer types. 

For example, the following constrained cast generates a compile-time error:

 #include <iostream>

 int main() {
     int x = 2;
     int* p;

     p = static_cast<int*>(x);  // FAILS: unrelated types 

     std::cout << p;
 }

Some static casts are portable across different platforms. 

Unrelated Types

The reinterpret_cast<Type>(expression) keyword converts the expression from its evaluated type to an unrelated type.  This cast may produce a value that has the same bit pattern as the evaluated expression. 

For example, to cast an int type to a pointer to an int type, we write:

 // Cast to an Unrelated Type
 // reinterpret_cast.cpp

 #include <iostream>

 int main( ) {
     int x = 2;
     int* p;

     p = reinterpret_cast<int*>(x);  // int and int* are unrelated 

     std::cout << p;
 }

reinterpret_cast<Type>(expression) performs minimal type checking.  It rejects conversions between related types. 

For example, the following constrained cast generates a compile-time error:

 #include <iostream>

 int main( ) {
     int x = 2;
     double y;

     y = reinterpret_cast<double>(x);  // FAILS types are related 

     std::cout << y;
 }

Few reinterpret casts are portable.  Uses include

  • evaluating raw data
  • recovering data where types are unknown
  • quick and messy calculations

Unmodifiable Types

The const_cast<Type>(expression) keyword removes the const status from an expression.

A common use case for this constrained cast is a function written by another programmer that does not receive a const parameter but should receive one.  If we cannot call the function with a const argument, we temporarily remove the const status and hope that the function is truly read only. 

 // Strip const status from an Expression
 // const_cast.cpp

 #include <iostream>

 void foo(int* p) {
     std::cout << *p << std::endl;
 }

 int main( ) {
     const int x = 3;
     const int* a = &x;
     int* b;

     // foo expects int* and not const int*
     b = const_cast<int*>(a);  // remove const status 
     foo(b);
 }

const_cast<Type>(expression) performs minimal type checking.  It rejects conversions between different types. 

For example, the following code generates a compile-time error:

 #include <iostream>

 int main( ) {
     const int x = 2;
     double y;

     y = const_cast<double>(x); // FAILS 

     std::cout << y;
 }

Inherited Types

The dynamic_cast<Type>(expression) keyword converts the value of an expression from its type to another type within the same class hierarchy and performs some type checking. 

Downcasts

dynamic_cast<Type>(expression) rejects a downcast from a base class pointer to a derived class pointer if a mismatch occurs or the object cannot be derived from the expression type.  The result of a dynamic cast must be tested to ensure that the conversion was successful.

For example:

 // Downcast within the Hierarchy
 // downcast.cpp

 #include <iostream>

 class Base {
   public:
     virtual void display() const { std::cout << "Base\n"; } 
 };
 class Derived : public Base {
   public:
     void display() const { std::cout << "Derived\n"; } 
 };

 int main( ) {
     Base* b1 = new Base;
     Base* b2 = new Derived;
     Derived* d1 = dynamic_cast<Derived*>(b1);
     Derived* d2 = dynamic_cast<Derived*>(b2)if (d1 != nullptr)
         d1->display();
     else
         std::cerr << "d1 is not derived" << std::endl;

     if (d2 != nullptr)
         d2->display();
     else
         std::cerr << "d2 is not derived" << std::endl;

     delete b1
     delete d2;
 }






















 d1 is not derived 
 Derived









Upcasts

dynamic_cast<Type>(expression) rejects an upcast from a derived class pointer to a base class pointer if a mismatch occurs or the object is not derived from the expression type.  The result of a dynamic cast must be tested to ensure that the conversion was successful.

For example, to cast a derived class pointer to a base object d to a pointer to its base class part, we write:

 // Upcast within the Hierarchy
 // upcast.cpp

 #include <iostream>

 class Base {
   public:
     void display() const { std::cout << "Base\n"; } 
 };
 class Derived : public Base {
   public:
     void display() const { std::cout << "Derived\n"; }
 };

 int main( ) {
     Base* b;
     Derived* d = new Derived;

     b = dynamic_cast<Base*>(d);  // in the same hierarchy 
     if (b != nullptr)
         b->display();
     else
         std::cerr << "Mismatch" << std::endl;
     d->display();
     delete d;
 }






















 Base
 Derived 


Note that here the display() member function is not virtual.  If it were, both calls to it would produce the same result.

Compile-Time Checking

dynamic_cast<Type>(expression) performs some compile-time type checking.  It rejects conversions from a base class pointer to a derived class pointer if the object is monomorphic; that is, if the base class is not a polymorphic type. 

For example, the following constrained cast generates a compile-time error:

 // Dynamic Cast - Compile Time Checking
 // dynamic_cast.cpp

 #include <iostream>

 class Base {
   public:
     void display() const { std::cout << "Base\n"; } 
 };
 class Derived : public Base {
   public:
     void display() const { std::cout << "Derived\n"; } 
 };

 int main( ) {
     Base* b = new Base;
     Derived* d;

     d = dynamic_cast<Derived*>(b);  // FAILS 
     b->display();
     d->display();
     delete d;
 }

Note that a static_cast works here and may produce the result shown on the right.  However, the Derived part of the object would then be incomplete.  static_cast does not check if the object is complete, leaving the responsibility to the programmer.

 // Static Cast - Compile Time Checking
 // static_cast.cpp

 #include <iostream>

 class Base {
   public:
     void display() const { std::cout << "Base\n"; }
 };
 class Derived : public Base {
   public:
     void display() const { std::cout << "Derived\n"; } 
 };

 int main( ) {
     Base* b = new Base;
     Derived* d;

     d = static_cast<Derived*>(b);  // OK 
     b->display();
     d->display();
     delete d;
 }



















 Base
 Derived 


Note that if display() is declared virtual the output may be the same for both calls to display().


Summary

  • a template header consists of the keyword template followed by the template parameters
  • the compiler generates the template specialization based on the argument types in the function call
  • avoid type casting that completely bypasses the language's type-checking facilities
  • if type casting is necessary, use one of the four type cast keywords (usually static_cast)

Exercises




Previous Reading  Previous: Abstract Base Classes Next: Overview of Polymorphism   Next Reading


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