CS202 Programming

Systems

 

Lecture Notes #7

 

 

 

Dynamic Binding

using

Virtual Functions

and

RTTI

 

 

Lecture and Reference


 

Static Binding:    Associating a member function with an object, pointer, or reference at compile time based on the static type of the object.

Static binding occurs when an object is associated with a member function based on the static type of the object. The static type of an object is the type of its class or the type of its pointer or reference. A member function statically bound to an object can be either a member of its class or an inherited member of a direct or indirect base class. Since static binding occurs at compile time, it is also called compile time binding.

In the following sections we will look at static binding using an object with the direct member access operator and a pointer to an object with the indirect member access operator. We will also look at legal assignment operations from one type of object to another when the objects are related by a common class hierarchy. Once this material is covered, we will be prepared to discuss pure polymorphism and dynamic binding.

 

7.1 Assigning Derived Class Objects to Base Class Objects

As we saw in the previous lecture, a publicly derived class represents an "is a" relationship. This means that whenever we need a direct or indirect base class object, we can use a derived class object in its place because a derived class object is a base class object. Figure 7-1 illustrates a hierarchy consisting of four classes, an account class, a checking class, a student class, and a savings class.

 

 

Figure 7-1: The Relationship between a Class Hierarchy and Derived Objects

 

Using this account class hierarchy, whenever we need an account object we can use a checking, student, or savings object in its place. This is because every checking, student, and savings object contains an account object. This is the essential characteristic of an inheritance hierarchy. Notice that it does not work the other way around. We cannot use an account object when we need a checking, student, or savings object because an account object does not have the necessary data members or member functions. If we attempt to do that a compile error will result.

The following example illustrates this by using a student object to initialize and assign to a checking object and an account object:

 

//account.h (Ex1701)

class account {

  public:

    account(const char* ="none", float=0);

    void statement();

  private:

    char name[32];   //account owner

    float balance;   //account balance

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

    void statement();

  private:

    float charges;   //charges for current month

};

 

class student : public checking {

  public:

    student(const char* ="none", float=0, const char* ="");

    void statement();

  private:

    char school[32]; //student's school

};

 

class savings : public account {

  public:

    savings(const char* ="none", float=0);

    void statement();

  private:

    float interest;  //interest for current month

};

 

//account.cpp (Ex1701)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

void checking::statement() {

  cout <<"Checking ";

  account::statement();

  cout <<"  charges = " <<charges <<endl;

}

 

student::student(const char* n, float b, const char* s) :

  checking(n, b, 0) {

  strncpy(school, s, 32);

  school[31] = '\0';

}

void student::statement() {

  cout <<"Student ";

  checking::statement();

  cout <<"  school = " <<school <<endl;

}

 

savings::savings(const char* n, float b) :

  account(n, b),

  interest(0) {

}

void savings::statement() {

  cout <<"Savings ";

  account::statement();

  cout <<"  interest = " <<interest <<endl;

}

 

//main.cpp (Ex1701)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_account(account a) {

  a.statement();

}

 

int main() {

  student reed("Kyle Reed", 5000, "UT");

 

  student s(reed);

  s.statement();

  cout <<endl;

  

  checking c;

  c = s;               //same effect as checking c(s);

  c.statement();

  cout <<endl;

 

  account a;

  a = s;               //same effect as checking a(s);

  a.statement();

  cout <<endl;

 

  print_account(reed); //pass account part by value

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Notice that when we use a student object to initialize or assign to a checking or account object, we lose the derived class parts. This is illustrated in Figure 7-2. When we then use the statement member function, the actual member function used is based on the type of the object. For the checking object, the checking's statement member function is used. For the account object, the account's statement member function is used. Both are statically bound by the compiler based on the type of the object.

 

 

Figure 7-2: Assignment of Derived Class Objects to Base Class Objects

 

Static binding guarantees that we will never associate a member function of a derived class with an object of a direct or indirect base class. If that were to happen, the member function would attempt to access data members that do not exist in the object. That is because a base class object does not have an "is a" relationship with a derived class object. Of course, we can go the other way around. That is, we can associate a member function of a direct or indirect base class with an object of a derived class as long as that member function is accessible (i.e., public). That is what inheritance is all about and it works because we have an "is a" relationship.

 

7.2 Assigning to an Array of Objects

The following example assigns a variety of derived class objects to an array where the type of the array is a direct or indirect base of the derived objects. Notice that this provides a way to access a common base class type for a heterogeneous collection of derived class objects.

 

//main.cpp (Ex1702)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_statements(account bank[], int n) {

  cout <<"Current Bank Account Statements:\n" <<endl;

  for(int i=0; i<n; ++i) {

    bank[i].statement();

    cout <<endl;

  }

}

 

int main() {

  savings i("Jim Jones", 500);

  account a("Empty Account", 0);

  student s("Kyle Reed", 5000, "UT");

  checking c("Sue Smith", 1000);

 

  account bank[4];

  bank[0] = i;

  bank[1] = a;

  bank[2] = s;

  bank[3] = c;

 

  print_statements(bank, 4);

  return (0);

}

 

This results in the following output:

 

Current Bank Account Statements:

 

Account Statement

  name = Jim Jones

  balance = 500

 

Account Statement

  name = Empty Account

  balance = 0

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Account Statement

  name = Sue Smith

  balance = 1000

 

The process of assigning various types of derived class objects to an array whose type is a direct or indirect base of the derived objects is illustrated in Figure 7-3. Notice that the derived class parts of each element are lost when they are stored in the array.

 

 

Figure 7-3: An Array of Derived Class Objects

 

In the next section we will look at assigning pointers to derived class objects to base class pointers. Once this section is understood, we will be ready to understand the true power of object-oriented programming by using dynamic binding.

 

7.3 Assigning Pointers to Derived Class Objects to Base Class Pointers

In the last section, we saw how we can assign a derived class object to a base class object when we have an "is a" relationship. In the same way, we can assign pointers to derived class objects to point to base class objects. We can also use derived class objects to initialize references to base class objects.

Refer back to Figure 7-1. Using this account class hierarchy, whenever we need a pointer to an account object, we can use a pointer to a checking, student, or savings object. This is called upcasting even though no cast operation is necessary. This is because every checking, student, and savings object contains an account object. When we are pointing to a checking, student, or savings object, we are also pointing to an account object. Notice that it does not work the other way around. When we are pointing to an account object, we do not have access to a checking, student, or savings object because an account object does not have the necessary data members or member functions.  If we attempt to do that a compile error will result.

The same thing applies with references. We can always use a derived class object to initialize a reference to a direct or indirect base class object. The following example illustrates the use of pointers and references to base class objects:

 

//main.cpp (Ex1703)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_account(account* p) { //pointer

  p->statement();

}

 

void print_account(account &r) { //reference

  r.statement();

}

 

int main() {

  student reed("Kyle Reed", 5000, "UT");

 

  student* ps = &reed;

  ps->statement();

  cout <<endl;

 

  checking* pc = &reed;

  pc->statement();

  cout <<endl;

 

  account* pa = &reed;

  pa->statement();

  cout <<endl;

 

  print_account(&reed); //pass by pointer

  cout <<endl;

 

  print_account(reed);  //pass by reference

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Notice that when we have a pointer to an account object and we initialize or assign it to the address of a student or checking object, that we are still actually pointing to the student or checking object. This is illustrated in Figure 7-4. This is the only time in C++ when it is allowed to assign a pointer of one type to another without an explicit cast operation. This is because a pointer to a derived class object points to a direct or indirect base class object as well!

 

 

Figure 7-4: Base and Derived Class Pointers to a Derived Class Object

 

When we use the statement member function with our account pointer, the actual member function used is the account's statement member function. This is because static binding is in effect. The member function bound by the compiler is based on the static type of the pointer, not the actual or dynamic type of the object pointed to. Thus, even though the complete derived class object is there, static binding prevents us from using the derived class' statement member function. To do that we need to use dynamic binding (Section 7.6).

 

7.4 Assigning to an Array of Pointers to Objects

The following example illustrates how we can assign the address of different derived class objects to an array of pointers where the type of the pointers is a direct or indirect base class of the derived class objects. Notice that this provides a way to access a heterogeneous collection of derived class objects in a homogeneous way. But, because of static binding, only the account part of the derived class objects are accessible.

 

//main.cpp (Ex1704)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_statements(account* bank[], int n) {

  cout <<"Current Bank Account Statements:\n" <<endl;

  for(int i=0; i<n; ++i) {

    bank[i]->statement();

    cout <<endl;

  }

}

 

int main() {

  savings i("Jim Jones", 500);

  account a("Empty Account", 0);

  student s("Kyle Reed", 5000, "UT");

  checking c("Sue Smith", 1000);

 

  account* bank[4];

  bank[0] = &i;

  bank[1] = &a;

  bank[2] = &s;

  bank[3] = &c;

 

  print_statements(bank, 4);

  return (0);

}

 

This results in the following output:

 

Current Bank Account Statements:

 

Account Statement

  name = Jim Jones

  balance = 500

 

Account Statement

  name = Empty Account

  balance = 0

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Account Statement

  name = Sue Smith

  balance = 1000

 

The output from the previous example contains only the account part of the derived class objects because the account's statement function is statically bound based on the type of the pointer (a pointer to an account object).

The array created in the previous example is illustrated in Figure 7-5. Notice that when we use pointers, the entire derived class object is pointed to even though the type of the pointer is to a direct or indirect base class object. The next section will show how to use dynamic binding to get at the derived class objects using a base class pointer.

 

 

Figure 7-5: An Array of Pointers to Derived Class Objects

 

The remainder of this lecture takes advantage of this ability to assign the address of a derived class object to a base class pointer. By taking advantage of the type of object actually pointed to instead of the static type of the pointer itself, we can design programs that are more reusable and extensible. This is the subject of the next two sections on virtual functions and dynamic binding.

 

 

7.5 Virtual Functions and Function Overriding

Pure Polymorphism:      Defining an interface for virtual member functions in a base class that are overridden in derived classes.

Hiding alternative functions behind a common interface is called polymorphism (a Greek term which means "many forms"). Polymorphism allows multiple implementations of a member function to be defined, each implementing different behavior. Member function overloading is one form of polymorphism. Member function hiding is another. One of the most powerful forms of polymorphism is member function overriding. With overriding, applications can be independent of derived classes by using only base class member functions to perform operations on derived classes. New derived classes can be added with no change to existing applications and those applications will automatically be able to process objects created from the new classes.

Within the object-oriented programming paradigm, function overriding takes function hiding to the next level. Instead of deciding which function to bind to an object based on its static type at compile time, the decision about which function to use is based on its dynamic type and is postponed until run time. This is called pure polymorphism. Pure polymorphism defines an interface to one or more virtual member functions in a base class that are overridden in derived classes. In the following sections, we will define function overriding and then compare it with function overloading and function hiding.

 

7.5.1 The Concept of Overriding

Overriding:     Defining a function to be virtual in a base class and then implementing that function in a derived class using exactly the same signature and return type.

Pure polymorphism allows member functions to be selected dynamically at run time. Instead of hiding member functions, we override them and delay the decision about which member function to use until run time. Member functions are overridden when a function is declared to be virtual in a direct or indirect base class and is then implemented in one or more derived classes using the same signature and return type. The selection of which function to use depends on the dynamic type of the object when accessed through a direct or indirect base class pointer or reference at run time.

Pure polymorphism requires four things to happen before it is in effect. First, we must have an inheritance hierarchy using public derivation. Second, we must declare a public member function to be virtual in either a direct or indirect base class. Third, an overridden member function implemented in a derived class must have exactly the same signature and return type as the virtual function declaration. Fourth, the overridden function must be accessed through a direct or indirect base class pointer or reference.

 

7.5.2 Comparing Overriding to Overloading

When we previously examined function overloading, we found that multiple versions of the same named function can be created, each with different meanings. These overloaded functions are specified within the same scope and must have unique signatures. The function selected depends on the overload resolution process. Overload resolution is done at compile time.

There are two major differences between overloading and overriding. Overloading requires unique signatures whereas overriding requires the same signature and return type. Second, overloading requires that each overloaded version of the function be specified within the same scope whereas overriding requires each overridden version be specified within the scope of each derived class.

 

7.5.3 Comparing Overriding to Hiding

When we previously examined inheritance, we found that member functions in derived classes can hide member functions of the same name in base classes. This allows a derived class to redefine a member function of a base class without modifying the code in the base class' implementation. With member function hiding, function signatures do not need to be unique.

Hiding happens whenever we have a derived class that has the same member name as a base class. When clients use derived class objects, the base class' member is hidden and can only be accessed by qualifying it using the class name and the scope resolution operator. Just like overloading, hiding is resolved at compile time.

There are two major differences between hiding and overriding. Hiding has no requirements on the signatures whereas overriding requires exactly the same signature and return type. Second, hiding uses the static type of the object at compile time to determine which member function to bind whereas overriding uses the dynamic type of the object at run time to determine which member function to bind.

 

 

7.6 Dynamic Binding

Dynamic Binding:     Associating a member function with a pointer or reference at run time based on the dynamic type of the object.

Dynamic binding occurs when a pointer or reference is associated with a member function based on the dynamic type of the object. The dynamic type of an object is the type of the object actually pointed or referred to rather than the static type of its pointer or reference. The member function that is dynamically bound must override a virtual function declared in a direct or indirect base class. Since dynamic binding occurs at run time, it is also called run time binding.

In the following sections we will look at dynamic binding using a pointer or reference to an object and the indirect member access operator. We will look at how virtual functions enable run time binding. Once this is covered, we will consider how to bypass the dynamic binding mechanism and also how to ensure that the dynamic binding mechanism is always used. Finally, we will cover the use of virtual destructors.

 

7.6.1 Syntax of Virtual Functions

Specifying the keyword virtual for any base class member function enables dynamic binding for that function. Any derived class can override that function by defining a function with the same signature and return type. The keyword virtual does not need to be re-specified within the derived class. Once a member function is declared to be virtual in a base class, all functions with that name, signature, and return type in any derived class remain virtual and can be overridden.

 

7.6.2 Enabling Dynamic Binding

Whenever we want a function to be dynamically bound, we should define that function as virtual in a direct or indirect base class. By doing so, we are turning on the dynamic binding mechanism and allowing member functions to be selected at run time based on the type of object pointed or referred to. Virtual functions should be used when we want to provide member functions in our base class that define an interface for application programs to use. The actual implementation of the virtual functions is either provided by the base class or is overridden and implemented as appropriate in derived classes. Providing such an interface in a base class allows application programs to be independent of knowledge of derived classes and allows additional derived classes to be created and used without modification to the application programs.

There are a few rules we must be aware of. First of all, virtual functions cannot be static member functions. Second, the signature and return type must be the same for all implementations of the virtual function. Third, while the function must be defined as a virtual function within a direct or indirect base class, it need not be defined in those derived classes where the inherited behavior does not need to differ. And finally, the keyword virtual is only required within the base class itself; derived class implementations of the overridden function do not need to repeat the use of that keyword. Once a member function is declared to be virtual, it remains virtual for all derived classes.

If the signature of the overridden function is not the same as the declaration of the virtual function, overriding does not occur and the virtual function is simply hidden. In such cases, the virtual function invoked will be an inherited function from a direct or indirect base class determined at compile time.

 

.7.6.3 Using Dynamic Binding

The following example demonstrates how we declare a member function to be virtual. This example is exactly the same as example Ex1703 except for declaring the statement member function to be virtual in the base class account:

 

//account.h (Ex1705)

class account {

  public:

    account(const char* ="none", float=0);

    virtual void statement(); //virtual function

  private:

    char name[32];

    float balance;

};

...

 

//main.cpp (Ex1705)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_account(account* p) { //pointer

  p->statement();

}

 

void print_account(account &r) { //reference

  r.statement();

}

 

int main() {

  student reed("Kyle Reed", 5000, "UT");

 

  student* ps = &reed;

  ps->statement();

  cout <<endl;

 

  checking* pc = &reed;

  pc->statement();

  cout <<endl;

 

  account* pa = &reed;

  pa->statement();

  cout <<endl;

 

  print_account(&reed); //pass by pointer

  cout <<endl;

 

  print_account(reed);  //pass by reference

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Compare these results with the output from example Ex1703. The simple syntactic change of adding the virtual keyword to the declaration of statement has significantly changed the output.

In this example, the member function statement is a virtual function. It is defined in the base class and is overridden in the derived classes. Notice that the signature and return types are the same. Also notice that the keyword virtual only occurs in the base class' definition. It is this declaration that enables dynamic binding. Finally, notice that we call the member function statement through a pointer or reference.

 

7.6.4 Arrays of Pointers to Base Classes

Dynamic binding allows a heterogeneous collection of objects to be processed in a homogeneous way. The true benefit of dynamic binding is achieved when programs can process different types of objects using a common interface. Our account class hierarchy and the statement member function is such an example. The account class defines the interface for the virtual statement function. The statement function is overridden in each derived classes and generates a statement appropriate for its class. The checking account statement includes a charge for checks written whereas the savings account statement includes a credit for interest that has accumulated. The statement member function has the same name and interface for each class, but results in different statements being generated when invoked through a direct or indirect base class pointer or reference.

Now, when we write an application to generate the statements for all types of accounts in our bank, all we need to do is to restrict the application to use only virtual functions defined in the account base class. The application has no knowledge about any derived class, yet it is able to generate statements appropriate for each type of account. Furthermore, if additional types of accounts are later added to the class hierarchy, the application will automatically be able to generate statements appropriate to those accounts without being modified. It will not even have to be recompiled since it is written entirely in terms of the interface presented by the base class.

This is illustrated in the following example where the statement member function is declared to be virtual in the account class. The statement member function is a simple example of what could be a much larger application written entirely in terms of virtual functions declared in the base class. This example is exactly the same as example Ex1704 except for declaring the statement member function to be virtual in the base class account:

 

//account.h (Ex1706)

class account {

  public:

    account(const char* ="none", float=0);

    virtual void statement(); //virtual function

  private:

    char name[32];

    float balance;

};

...

 

//main.cpp (Ex1706)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_statements(account* bank[], int n) {

  cout <<"Current Bank Account Statements:\n" <<endl;

  for(int i=0; i<n; ++i) {

    bank[i]->statement();

    cout <<endl;

  }

}

 

int main() {

  savings i("Jim Jones", 500);

  account a("Empty Account", 0);

  student s("Kyle Reed", 5000, "UT");

  checking c("Sue Smith", 1000);

 

  account* bank[4];

  bank[0] = &i;

  bank[1] = &a;

  bank[2] = &s;

  bank[3] = &c;

 

  print_statements(bank, 4);

  return (0);

}

 

This results in the following output:

 

Current Bank Account Statements:

 

Savings Account Statement

  name = Jim Jones

  balance = 500

  interest = 0

 

Account Statement

  name = Empty Account

  balance = 0

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Checking Account Statement

  name = Sue Smith

  balance = 1000

  charges = 5

 

Compare these results with the output from example Ex1704. The dynamic binding mechanism that allows this to work is not magic, but it does cause some overhead to be introduced into the member function call. The next section looks at how dynamic binding works and what it costs to use.

 

 

7.6.5 The Dynamic Binding Mechanism and its Costs

Dynamic binding delays until run time the binding of a member function to a pointer or reference. This requires that the compiler generate code to select the correct member function at run time instead of compile time. There are several different ways that compilers implement the dynamic binding mechanism.

One way that dynamic binding is often implemented is to create an array of member function pointers for all functions that are declared to be virtual. Each derived class has its own unique array of member function pointers. Functions that are inherited result in pointers to direct or indirect base class member functions. Functions that are overridden result in pointers to the derived class member functions. Each virtual function has the same index in this table for each derived class. Only one table exists per class that is shared by all objects created from this class.

This array of member function pointers is sometimes called a vtbl (virtual table) or vrt (virtual resolution table). Each derived class object contains its own pointer to this virtual table for its class. This pointer is sometimes called a vptr (virtual pointer). When a member function is to be bound to a pointer or reference at run time, the function accessed is obtained by selecting the correct member function pointer out of the virtual table pointed to by the current object's virtual pointer. It doesn't matter what the type of the object is, its virtual pointer will point to the correct virtual table of function pointers for that object.

The following diagram illustrates this for the previous example:

 

 

Figure 7-6: A Dynamic Binding Mechanism using vptrs and vtbls

 

Note the additional costs associated with using dynamic binding instead of static binding. With static binding, a member function is directly bound to an object. With this implementation of dynamic binding, three additional levels of indirection are needed to bind the correct member function pointer with a pointer or reference to an object. First, the pointer to the object must be dereferenced to access the vptr. Second, the vptr must be dereferenced to access the correct vtbl. Third, the member function pointer in the vtbl must be accessed in order to then call the correct member function for that object.

This cost is not as bad as it first seems. First, many compilers do this more efficiently that we have described here. Second, if the dynamic binding mechanism were not being used, then our application would be implemented significantly differently. Each derived class would have to define a value to represent its type so that the application could query the object at run time to determine what type it was pointing to. Then, the application would have to implement a switch statement or series of if/else checks to downcast the pointer from an account object to the correct type in order to access the correct derived class member function. This would have to be done each time a derived class member function needed to be accessed. In addition to all of that overhead, the application would be difficult to maintain and error prone when later adding new derived classes.

The following example is identical to the previous example except that it contains an explicit implementation of a vptr in the base class and vtbl's in the base class and each derived class. The statement member function is not declared to be virtual. Rather, in the main program, it is referenced indirectly using the vptr and vtbl for the current object. The purpose of this example is to explicitly show how the virtual function mechanism works as illustrated in Figure 7-6.

 

//account.h (Ex1707)

class account {

  public:

    account(const char* ="none", float=0);

    void statement();

    void (account::**vptr)(...);          //pointer to vtbl

  private:

    static void (account::*vtbl[])(...);  //vtbl

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

    void statement();

  private:

    static void (checking::*vtbl[])(...); //vtbl

    float charges;

};

 

class student : public checking {

  public:

    student(const char* ="none", float=0, const char* ="");

    void statement();

  private:

    static void (student::*vtbl[])(...);  //vtbl

    char school[32];

};

 

class savings : public account {

  public:

    savings(const char* ="none", float=0);

    void statement();

  private:

    static void (savings::*vtbl[])(...);  //vtbl

    float interest;

};

 

//account.cpp (Ex1707)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

void (account::*account::vtbl[])(...) =

  {&account::statement};  //initialize account class vtbl

 

account::account(const char* n, float b) :

  balance(b) {

  vptr = vtbl;

  strncpy(name, n, 32);

  name[31] = '\0';

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

 

void (checking::*checking::vtbl[])(...) =

  {&checking::statement}; //initialize checking class vtbl

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

  vptr = reinterpret_cast<void (account::**)()>(vtbl);

}

void checking::statement() {

  cout <<"Checking ";

  account::statement();

  cout <<"  charges = " <<charges <<endl;

}

 

void (student::*student::vtbl[])(...) =

  {&student::statement};  //initialize student class vtbl

 

student::student(const char* n, float b, const char* s) :

  checking(n, b, 0) {

  vptr = reinterpret_cast<void (account::**)()>(vtbl);

  strncpy(school, s, 32);

  school[31] = '\0';

}

void student::statement() {

  cout <<"Student ";

  checking::statement();

  cout <<"  school = " <<school <<endl;

}

 

void (savings::*savings::vtbl[])(...) =

  {&savings::statement};  //initialize savings class vtbl

 

savings::savings(const char* n, float b) :

  account(n, b),

  interest(0) {

  vptr = reinterpret_cast<void (account::**)()>(vtbl);

}

void savings::statement() {

  cout <<"Savings ";

  account::statement();

  cout <<"  interest = " <<interest <<endl;

}

 

//main.cpp (Ex1707)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_statements(account* bank[], int n) {

  cout <<"Current Bank Account Statements:\n" <<endl;

  for(int i=0; i<n; ++i) {

    (bank[i]->*(bank[i]->vptr[0]))(); //virtual call

    cout <<endl;

  }

}

 

int main() {

  savings i("Jim Jones", 500);

  account a("Empty Account", 0);

  student s("Kyle Reed", 5000, "UT");

  checking c("Sue Smith", 1000);

 

  account* bank[4];

  bank[0] = &i;

  bank[1] = &a;

  bank[2] = &s;

  bank[3] = &c;

 

  print_statements(bank, 4);

  return (0);

}

 

This results in the following output:

 

Current Bank Account Statements:

 

Savings Account Statement

  name = Jim Jones

  balance = 500

  interest = 0

 

Account Statement

  name = Empty Account

  balance = 0

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Checking Account Statement

  name = Sue Smith

  balance = 1000

  charges = 5

 

Compare this program with Ex1706. The results are the same, but all we had to do in Ex1706 was to declare the statement member function to be virtual in the base class. Also, note that the mechanism we have implemented in this example is not as general as what the compiler can do for us. This implementation is restricted to using member functions that return void. When using virtual functions, the compiler does not impose that restriction on us.

 

7.6.6 Disabling Dynamic Binding

Once dynamic binding is enabled, an overridden member function is bound to a pointer or reference based on the type of the object pointed to. Dynamic binding is not in effect if an object is used instead of a pointer or reference to an object. Dynamic binding is also not in effect if the member function is qualified with a class name and the scope resolution operator. In this case, the function bound to the pointer is the member function defined by the qualifying class.

The following example illustrates both of these ways to disable dynamic binding.

 

//main.cpp (Ex1708)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  student reed("Kyle Reed", 5000, "UT");

 

  account* pa = &reed;

  pa->statement();          //dynamically bind

  cout <<endl;

 

  account a = reed;

  a.statement();            //statically bind

  cout <<endl;

 

  pa->account::statement(); //statically bind

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

Account Statement

  name = Kyle Reed

  balance = 5000

 

The result using an account pointer with statement (dynamic binding) uses the student statement member function. However, if we assign the student object to an account object and then invoke statement, static binding is in effect and the account statement member function is called. When we use an account pointer and qualify statement with the account class and the scope resolution operator, static binding is also in effect and the account statement member function is called. (i.e., dynamic binding not used).

 

7.6.7 Protected or Private Virtual Functions

We can force application programs to always use pointers to base class objects instead of using pointers to derived class objects or using objects directly by making the overridden member functions protected or private in the derived classes. This is a way to force applications to adhere to the defined interface provided by the base class and to help ensure that dynamic binding will be used.

This is illustrated in the following example:

 

//account.h (Ex1709)

class account {

  public:

    account(const char* ="none", float=0);

    virtual void statement();

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

  protected:

    void statement();

  private:

    float charges;

};

 

class student : public checking {

  public:

    student(const char* ="none", float=0, const char* ="");

  protected:

    void statement();

  private:

    char school[32];

};

 

//main.cpp (Ex1709)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  student reed("Kyle Reed", 5000, "UT");

 

  account* pa = &reed;

  pa->statement();  //okay

  cout <<endl;

 

  student* ps = &reed;

  ps->statement();  //illegal access-protected member

  cout <<endl;

 

  reed.statement(); //illegal access-protected member

  return (0);

}

 

7.6.8 Making Non-Member Functions Virtual

Virtual Friend:    A non-member function that uses a virtual member function to implement its operation.

Dynamic binding can be used with overloaded operators as well as conversion functions. All that is necessary is to declare the overloaded operators to be virtual in a direct or indirect base class just like we do for regular member functions. Functions and overloaded operators that cannot be implemented as members can benefit from dynamic binding by invoking a virtual member function that actually performs the required operation. When this is done, such functions are called virtual friends.

In order to implement a virtual friend, we must implement a virtual helper member function that performs the operation that we want and then call it from the non-member function. We must be careful to declare the object for which we want polymorphic behavior to be a pointer or a reference in the helper function. If the object is passed by value to the non-member function then dynamic binding cannot be used because we have an object and not a pointer or reference to an object.

We can force the non-member function to be used by the client application instead of the helper member function by making the helper function protected or private in the base class and in all derived classes and then making the non-member function a friend of the class.

The following example illustrates this by overloading the insertion operator to perform output polymorphically for account objects.

 

//account.h (Ex1710)

#include <ostream>

 

class account {

friend std::ostream &operator<<(std::ostream &, account &);

  public:

    account(const char* ="none", float=0);

  protected:

    virtual void statement(std::ostream &);

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

  protected:

    void statement(std::ostream &);

  private:

    float charges;

};

 

class student : public checking {

  public:

    student(const char* ="none", float=0, const char* ="");

  protected:

    void statement(std::ostream &);

  private:

    char school[32];

};

 

//account.cpp (Ex1710)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

void account::statement(ostream &o) {

  o <<"Account Statement" <<endl;

  o <<"  name = " <<name <<endl;

  o <<"  balance = " <<balance <<endl;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

void checking::statement(ostream &o) {

  o <<"Checking ";

  account::statement(o);

  o <<"  charges = " <<charges <<endl;

}

 

student::student(const char* n, float b, const char* s) :

  checking(n, b, 0) {

  strncpy(school, s, 32);

  school[31] = '\0';

}

void student::statement(ostream &o) {

  o <<"Student ";

  checking::statement(o);

  o <<"  school = " <<school <<endl;

}

 

ostream &operator<<(ostream &o, account &a) {

  a.statement(o);

  return (o);

}

 

//main.cpp (Ex1710)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  student reed("Kyle Reed", 5000, "UT");

  account &ra(reed);  //reference

  account* pa(&reed); //pointer

 

  cout <<ra <<endl;

  cout <<*pa <<endl;

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

 

 

 

 

7.6.9 Virtual Destructors

Whenever we have virtual functions, we should always declare the destructor to be virtual. Making the destructor virtual will ensure that the correct destructor will be called for an object when pointers to direct or indirect base classes are used. If the destructor is not declared to be virtual, then the destructor is statically bound based on the type of pointer or reference and the destructor for the actual object pointed to will not be called when the object is deallocated.

In the following example, we have dynamically allocated memory to hold the name of the school in the student class. A destructor has been defined to deallocate that memory. The base class destructor has been declared to be virtual so that derived class destructors will be called when the object is deallocated. If the base class destructor had not been declared to be virtual, then the student destructor would not have been called and we would have had a memory leak.

We have added output statements to the destructors in the following classes to help show when the destructors are called.

 

//account.h (Ex1711)

#include <ostream>

 

class account {

friend std::ostream &operator<<(std::ostream &, account &);

  public:

    account(const char* ="none", float=0);

    virtual ~account();

  protected:

    virtual void statement(std::ostream &);

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

  protected:

    void statement(std::ostream &);

  private:

    float charges;

};

 

class student : public checking {

  public:

    student(const char* ="none", float=0, const char* ="");

    student(const student &);

    ~student();

    student &operator=(const student &);

  protected:

    void statement(std::ostream &);

  private:

    char* school;

};

 

//account.cpp (Ex1711)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

account::~account() {

}

void account::statement(ostream &o) {

  o <<"Account Statement" <<endl;

  o <<"  name = " <<name <<endl;

  o <<"  balance = " <<balance <<endl;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

void checking::statement(ostream &o) {

  o <<"Checking ";

  account::statement(o);

  o <<"  charges = " <<charges <<endl;

}

 

student::student(const char* n, float b, const char* s) :

  checking(n, b, 0) {

  school = new char[strlen(s) + 1];

  strcpy(school, s);

}

student::student(const student &s) :

  checking(s) {

  school = new char[strlen(s.school) + 1];

  strcpy(school, s.school);

}

student::~student() {

  delete[] school;

  cout <<"student destructor called" <<endl;

}

student &student::operator=(const student &s) {

  if (this != &s) {

    static_cast<checking &>(*this) = s;

    delete[] school;

    school = new char[strlen(s.school) + 1];

    strcpy(school, s.school);

  }

  return(*this);

}

void student::statement(ostream &o) {

  o <<"Student ";

  checking::statement(o);

  o <<"  school = " <<school <<endl;

}

 

ostream &operator<<(ostream &o, account &a) {

  a.statement(o);

  return (o);

}

 

//main.cpp (Ex1711)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  account* p = new student("Jan Reed", 750, "UNM");

  cout <<*p <<endl;

 

  delete p;

  cout <<endl;

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Jan Reed

  balance = 750

  charges = 0

  school = UNM

 

student destructor called

 

 

7.6.10 Virtual Inheritance and Virtual Functions

When using virtual inheritance, virtual functions can be used in the same way that we learned about previously. However, a class derived from two or more base classes that have a virtual base class in common must override all virtual functions declared in the common base class if it is overridden in more than one of its direct base class branches. If virtual functions are not overridden in the derived class, it is impossible to know which of the virtual functions to use. It would be impossible to know which direct base class' virtual function to use! This is because the virtual function is accessed from the common base class pointer pointing to a derived class object.  A compile error would indicate that no final overriding function has been defined.

A class derived from two or more base classes that have a virtual base class in common and where only one of the base classes has overridden a virtual function from the common base class is allowed. In such situations, the derived class need not provide a definition for this functions. The virtual function that is overridden in the one base class will dominate and will be used. This is called the dominance rule.

The following example makes the get_balance member function in account virtual. That function is then overridden in both the savings class and the checking class to compute the balance correctly for those classes that charge fees and compound interest. Because it has been overridden in more the one of its direct base classes, it must be overridden in the ibc derived class. If it is isn't, a compile error will result.

 

//account.h (Ex1806)

class account {

  public:

    account(const char* ="none", float=0);

    const char* get_name();

    virtual float get_balance();

  private:

    char name[32];

    float balance;

};

 

class checking : virtual public account {

  public:

    checking(const char* ="none", float=0, float=5);

    float get_balance();

    float get_charges();

  private:

    float charges;

};

 

class savings : virtual public account {

  public:

    savings(const char* ="none", float=0);

    float get_balance();

    float get_interest();

  private:

    float interest;

};

 

class ibc : public checking, public savings {

  public:

    ibc(const char* ="none", float=0, float=1000);

    float get_balance();

    float get_minimum();

  private:

    float minimum;

};

 

//account.cpp (Ex1806)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

const char* account::get_name() {

  return (name);

}

float account::get_balance() {

  return (balance);

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

float checking::get_balance() {

  return (account::get_balance() - charges);

}

float checking::get_charges() {

  return (charges);

}

 

savings::savings(const char* n, float b) :

  account(n, b),

  interest(b*.01) {

}

float savings::get_balance() {

  return (account::get_balance() + interest);

}

float savings::get_interest() {

  return (interest);

}

 

ibc::ibc(const char* n, float b, float m) :

  account(n, b),   //must call from the most derived class

  checking(n, m),  //no effect on account, only checking

  savings(n, b-m), //no effect on account, only savings

  minimum(m) {

}

float ibc::get_balance() {

  float balance = account::get_balance();

  float interest = (balance - minimum) * .01;

  float charges = get_charges();

  return (balance + interest - charges);

}

float ibc::get_minimum() {

  return (minimum);

}

 

//main.cpp (Ex1806)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  ibc i("Harry Hines", 3000);

  account* pa = &i;

 

  cout <<"IBC Account Statement" <<endl;

 

  cout <<"  name = "    <<pa->get_name() <<endl;

  cout <<"  balance = " <<pa->get_balance();

  return (0);

}

 

This results in the following output:

 

IBC Account Statement

  name = Harry Hines

  balance = 3015

 

Notice that the virtual function called is the function overridden in the icb class. If the icb class had not overridden the get_balance function, it would have been ambiguous whether to use the savings get_balance function or the checking get_balance function.

 

 

7.6.11 Multiple Inheritance and Virtual Functions

When using multiple inheritance, virtual functions can be used in the same way that we learned about previously. A class derived using multiple inheritance can override a virtual function from one or more of its direct base class branches.

The following example modifies the statement member function in both account and equity to be virtual. That function is then overridden in the assets class. It can now be accessed via a pointer to either an account or equity object.

 

//account.h (Ex1803)

class account {

  public:

    account(const char* ="none", float=0);

    virtual void statement();

    const char* get_name();

    float get_balance();

  private:

    char name[32];

    float balance;

};

...

 

//equity.h (Ex1803)

class equity {

  public:

    equity(const char* ="none", float=0);

    virtual void statement();

    const char* get_name();

    float get_holdings();

  private:

    char name[32];

    float holdings;

};

 

//main.cpp (Ex1803)

#include <iostream>

using namespace std;

#include "assets.h"

 

int main() {

  assets a("Pete Peterson", 500, 3000);

  account* pa = &a;

  equity* pe = &a;

 

  pa->statement(); //accessed as pointer to account

  pe->statement(); //accessed as pointer to equity

  return (0);

}

 

 

 

This results in the following output:

 

Assets Statement

  name = Pete Peterson

  total assets = 3500

Assets Statement

  name = Pete Peterson

  total assets = 3500

 

Notice that the output is the same whether we use a pointer to an account object or a pointer to an equity object. This is because in both cases we are pointing to the same assets object and are using the same virtual member function (statement) to access that object.

 

 

 

7.7 Run Time Type Identification

Run Time Type Identification:       Using direct or indirect base class pointers or references to determine at run time the actual type of a derived class object.

Run Time Type Identification (RTTI) uses type information stored in objects by the compiler to determine at run time the actual type of an object pointed or referred to. RTTI can only be used when at least one function has been declared to be virtual in the class or in a direct or indirect base class.

The following sections first consider the use of the static_cast operator. This operator can be used to downcast a direct or indirect base class pointer and convert it to a derived class pointer. It is inherently unsafe because no run time type information is used. Next we discuss the dynamic_cast operator. This operator is used to downcast a direct or indirect base class pointer and convert it to a derived class pointer using type information stored in the object. It is type safe because it uses run time type information. Finally, we discuss the use of the typeid operator to compare the type of an object using type information stored in the object with the type of a class.

 

7.7.1 Downcasting Using the static_cast Operator

Downcasting:     Converting a direct or indirect base class pointer to a derived class pointer.

Here, we look at uses of the static_cast operator to perform downcast operations. A downcast operation is when a pointer to a direct or indirect base class is converted to a pointer to a derived class.

The static_cast operator syntax is static_cast<type*>(expr) where expr is an expression pointing to a direct or indirect base class object and type is a derived class type. The result of the cast operation is that expr is converted to a pointer to the specified type. The static_cast operator simply converts the static type of one pointer into another. As such, it depends on the programmer to ensure that such a conversion is correct. It is inherently unsafe because no run time type information is used.

The following example uses static_cast to perform a downcast operation:

 

class account {

};

class checking : public account {

};

class student : public checking {

};

class savings : public account {

};

student s;

account* pa = &s;

student* ps;

ps = static_cast<student*>(pa); //result is valid pointer

 

savings i;

pa = &i;

ps = static_cast<student*>(pa); //result is a bad pointer

 

In this example, when pa points to a student object, the resulting pointer in ps is valid. However, when pa points to a savings object, the resulting pointer in ps ends up pointing to an object of the wrong type. Attempts to access information using the ps pointer would be disastrous. The static_cast operator requires that the client know the type of object being pointed to and relies entirely on the programmer to ensure that the pointers will be correct after the cast operation.  The static_cast operator is not a safe mechanism because no run time check is made and a pointer is always returned, even if it is incorrect for the type of object pointed to.

The next section discusses the dynamic_cast operator and shows how we can perform the same operation in a type safe way.

 

7.7.2 Type Safe Downcasting Using the dynamic_cast Operator

The dynamic_cast operator relies on information stored in an object by the compiler whenever a direct or indirect base class contains a virtual function. This cast is used when downcasting to determine at run time if the downcast is valid or not.

The dynamic_cast operator syntax is dynamic_cast<type*>(expr) where expr is an expression pointing to a direct or indirect base class object and type is a derived class type. The result of the cast operation is that expr is converted to a pointer of the specified type if the dynamic type of the object pointed to is that type or is a type derived from that type. Otherwise, the result of the cast operation is zero. This is demonstrated in the following example:

 

class account {

  public:

    virtual ~account(){}; //need a virtual function

};

class checking : public account {

};

class student : public checking {

};

class savings : public account {

};

student s;

account* pa = &s;

student* ps;

ps = dynamic_cast<student*>(pa); //result is valid pointer

 

savings i;

pa = &i;

ps = dynamic_cast<student*>(pa); //result is a zero pointer

 

In this example, when pa points to a student object, the resulting pointer in ps is valid. However, when pa points to a savings object, the resulting pointer in ps is zero. The dynamic_cast operator relies on the dynamic type information stored in the object pointed to. Unlike static_cast, the dynamic_cast operator provides a safe downcast mechanism by returning a zero pointer if the object pointed to is not in the hierarchy of the type being cast to. If it is, a valid pointer is returned.

 

7.7.3 Determining the Type of an Object Using the typeid Operator

The typeid operator relies on information stored in an object by the compiler whenever a direct or indirect base class contains a virtual function. This operator is used at run time to compare the type of an object with the type of a known class.

The typeid operator syntax is typeid(type) or typeid(expr) where type is a class type and expr is an expression resulting in an object. The typeid operator returns a reference to an object of type type_info. This object contains a compiler dependent run time representation for a particular type. This object can be compared with the type_info of known classes to determine the type of an object at run time. The header file <typeinfo> must be included to use the typeid operator. This is demonstrated in the following example:

 

#include <typeinfo>

class account {

  public:

    virtual ~account(){}; //need a virtual function

};

class checking : public account {

};

class student : public checking {

};

class savings : public account {

};

 

student* ps = new student;

if (typeid(*ps) == typeid(account))

  cout <<"*ps is an account object" <<endl;

if (typeid(*ps) == typeid(checking))

  cout <<"*ps is a checking object" <<endl;

if (typeid(*ps) == typeid(student))

  cout <<"*ps is a student object" <<endl;

if (typeid(*ps) == typeid(savings))

  cout <<"*ps is a savings object" <<endl;

 

The typeid operator is used to check the type of an object against a known type whereas the dynamic_cast operator can be used to check the type of an object against an entire hierarchy.

 

7.7.3 Example using RTTI

To get the full benefits of dynamic binding, client applications must be able to write code independent of the type of object being pointed or referred to. There are times, however, when client applications may need to know what type of object is being used. RTTI provides a way for client applications to determine the type of an object without having to compromise their use of dynamic binding. The dynamic_cast operator can be used to determine which hierarchies an object belongs to by comparing its type against known class hierarchies. The typeid operator can be used to determine the type of an object by comparing it against known classes.

The following is a complete example using both the dynamic_cast and typeid operators to determine the type of an object pointed to by a direct or indirect base class pointer:

 

//main.cpp (Ex1712)

#include <iostream>

#include <typeinfo>

using namespace std;

#include "account.h"

 

void print_hierarchy(account*);

void print_type(account*);

 

int main() {

  account* bank[4];

  bank[0] = new savings("Jim Jones", 500);

  bank[1] = new account("Empty Account", 0);

  bank[2] = new student("Kyle Reed", 5000, "UT");

  bank[3] = new checking("Sue Smith", 1000);

 

  for(int i=0; i<4; ++i) {

    print_hierarchy(bank[i]);

    print_type(bank[i]);

    cout <<endl;

  }

 

  for(int i=0; i<4; ++i) {

    delete bank[i];

  }

  return (0);

}

 

void print_hierarchy(account* object) {

  account* pa;

  pa = dynamic_cast<account*>(object);

  if (pa) {

    cout <<"object is an account" <<endl;

    checking* pc;

    pc = dynamic_cast<checking*>(object);

    if (pc) {

      cout <<"object is a checking" <<endl;

      student* ps;

      ps = dynamic_cast<student*>(object);

      if (ps) {

        cout <<"object is a student" <<endl;

      }

    }

    else {

      savings* pi;

      pi = dynamic_cast<savings*>(object);

      if (pi) {

        cout <<"object is a savings" <<endl;

      }

    }

  }

}

 

void print_type(account* object) {

  if (typeid(*object) == typeid(account)) {

    cout <<"object's type is account" <<endl;

  }

  if (typeid(*object) == typeid(checking)) {

    cout <<"object's type is checking" <<endl;

  }

  if (typeid(*object) == typeid(student)) {

    cout <<"object's type is student" <<endl;

  }

  if (typeid(*object) == typeid(savings)) {

    cout <<"object's type is savings" <<endl;

  }

}

 

This results in the following output:

 

object is an account

object is a savings

object's type is savings

 

object is an account

object's type is account

 

object is an account

object is a checking

object is a student

object's type is student

 

object is an account

object is a checking

object's type is checking

 

 

Notice that the dynamic_cast operator is able to determine whether or not an object is a member of a hierarchy. This can be useful to determine the ancestors of a class. On the other hand, the typeid operator is only able to determine if an object is of a particular type or not.

It is best to avoid using RTTI because it inherently ties the application to the types of objects that it is processing. Writing code independent of the type being pointed or referred to provides cleaner code that will be easier to maintain. Designs that need to use RTTI should act as a warning signal that a better design using virtual functions may be possible. If such a design is not possible, then use of the dynamic_cast and typeid operators is much preferred over the use of the static_cast operator because of the run time type checking.

 

 

7.8 Abstract classes

Abstract class:      A class from which no objects can be instantiated.

An abstract class is a class that can only be derived from; no objects can be instantiated from an abstract class. Its purpose is to define an interface and provide a common base class for derived classes. A base class becomes an abstract class either by making its constructor(s) protected or by declaring a virtual function to be pure.

The account class in our previous examples is a candidate for an abstract class. Its only purpose is to provide a common class from which the checking and savings classes can be derived. There is no reason to allow an object of type account to be created because an account is an abstract concept and does not represent a real world object. There are only specific types of accounts such as checking accounts and savings accounts.

The following sections discuss pure virtual functions and then provide an example of an abstract class.

 

7.8.1 Pure Virtual Functions

A pure virtual function is a virtual function declared to be pure. A class that declares a virtual function to be pure becomes an abstract class. Derived classes must implement all pure virtual functions. If a derived class does not implement these functions, then it becomes an abstract class as well. Abstract classes are not required to implement their pure virtual functions. If they are implemented in the abstract class, they can only be used by derived classes. This is usually done to provide common behavior for derived classes.

A function is declared to be pure by following its declaration with an equal sign and zero.  For example:

 

class account {

  public:

  virtual void statement() = 0;

};

 

This declares statement to be a pure virtual function and makes account an abstract class.

The purpose of declaring a function to be pure is to force the derived classes to implement it. A virtual function is a contract with a derived class indicating the name, signature, and return type for the function. Making the virtual function pure forces the contract to be fulfilled.

 

7.8.2 Using Pure Virtual Functions and Abstract classes

The following example modifies our previous account class hierarchy to make statement a pure virtual function and account an abstract class.

 

//account.h (Ex1713)

class account {

  public:

    account(const char* ="none", float=0);

    virtual void statement() = 0; //pure virtual function

  private:

    char name[32];

    float balance;

};

 

//main.cpp (Ex1713)

#include <iostream>

using namespace std;

#include "account.h"

 

void print_statements(account* bank[], int n) {

  cout <<"Current Bank Account Statements:\n" <<endl;

  for(int i=0; i<n; ++i) {

    bank[i]->statement();

    cout <<endl;

  }

}

 

int main() {

  account a("Empty Account", 0); //error-abstract class

  savings i("Jim Jones", 500);

  student s("Kyle Reed", 5000, "UT");

  checking c("Sue Smith", 1000);

 

  account* bank[4];

  bank[0] = &i;

  bank[1] = &s;

  bank[2] = &c;

 

  print_statements(bank, 3);

  return (0);

}

 

When we try to instantiate an object of type account, we get a compile error because account is an abstract class. If that statement is deleted, then the program works and we can instantiate objects of type checking, student, and savings because they have overridden the pure virtual statement function and are therefore not abstract classes.