Coronado Enterprises C++ TUTOR: Chapter 9
Chapter 9: MULTIPLE INHERITANCE AND FUTURE DIRECTIONS
C++ version 2.0 was released by AT&T during the summer of 1989, and the major addition to the language is multiple inheritance, the ability to inherit data and methods from more than one class into a subclass. Multiple inheritance and a few of the other additions to the language will be discussed in this chapter along with some of the expected future directions of the language.
Several companies have C++ compilers available in the marketplace, and many others are sure to follow. Because the example programs in this tutorial are designed to be as generic as possible, most should be compilable with any good quality C++ compiler provided it follows the AT&T definition of version 2.1 or newer. Many of these examples will not work with earlier definitions because the language was significantly changed with the version 2.1 update.
After completing this tutorial, you should have enough experience with the language to study additional new constructs on your own as they are implemented by the various compiler writers. We will update the entire tutorial as soon as practical following procurement of any new compiler, but hopefully the language will not change rapidly enough now to warrant an update oftener than annually. Please feel free to contact us for information on updates to the Coronado Enterprises C++ tutorial.
MULTIPLE INHERITANCE
A major recent addition to the C++ language is the ability to inherit methods and variables from two or more parent classes when building a new class. This is called multiple inheritance, and is purported by many people to be a major requirement for an object oriented programming language. Some writers, however, have expressed doubts as to the utility of multiple inheritance. To illustrate the validity of this, it was not easy to think up a good example of the use of multiple inheritance as an illustration for this chapter. In fact, the resulting example is sort of a forced example that really does nothing useful. It does however, illustrate the mechanics of the use of multiple inheritance with C++, and that is our primary concern at this time.
The biggest problem with multiple inheritance involves the inheritance of variables or methods from two or more parent classes with the same name. Which variable or method should be chosen as the inherited variable or method if two or more have the same name?
This will be illustrated in the next few example programs.
SIMPLE MULTIPLE INHERITANCE
An examination of the file named MULTINH1.CPP will reveal the definition of two very simple classes in lines 4 through 27 named moving_van and driver.
In order to keep the program as simple as possible, all of the member methods are defined as inline functions. This puts the code for the methods where it is easy to find and study. You will also notice that all variables in both classes are declared to be protected so they will be readily available for use in any class which inherits them. The code for each class is kept very simple so that we can concentrate on studying the interface to the methods rather than spending time trying to understand complex methods. As mentioned previously, chapter 12 will illustrate the use of non- trivial methods.
In line 30, we define another class named driven_truck which inherits all of the data and all of the methods from both of the previously defined classes. In the last two chapters, we studied how to inherit a single class into another class, and to inherit two or more classes, the same technique is used except that we use a list of inherited classes separated by commas as illustrated in line 30. The observant student will notice that we use the keyword public prior to the name of each inherited class in order to be able to freely use the methods within the subclass. In this case, we didn't define any new variables, but we did introduce two new methods into the subclass in lines 32 through 39.
We declared an object named chuck_ford which presumably refers to someone named Chuck who is driving a Ford moving van. The object named chuck_ford is composed of four variables, three from the moving_van class, and one from the driver class. Any of these four variables can be manipulated in any of the methods defined within the driven_truck class in the same way as in a singly inherited situation. A few examples are given in lines 47 through 56 of the main program and the diligent student should be able to add additional output messages to this program if he understands the principles involved.
All of the rules for private or protected variables and public or private method inheritance as used with single inheritance extends to multiple inheritance.
MULTINH1.CPP
// Chapter 9 - Program 1
#include <iostream.h>
class moving_van {
protected:
float payload;
float gross_weight;
float mpg;
public:
void initialize(float pl, float gw, float in_mpg) {
payload = pl;
gross_weight = gw;
mpg = in_mpg; };
float efficiency(void) {
return(payload / (payload + gross_weight)); };
float cost_per_ton(float fuel_cost) {
return(fuel_cost / (payload / 2000.0)); };
};
class driver {
protected:
float hourly_pay;
public:
void initialize(float pay) {hourly_pay = pay; };
float cost_per_mile(void) {return(hourly_pay / 55.0); } ;
};
class driven_truck : public moving_van, public driver {
public:
void initialize_all(float pl, float gw, float in_mpg, float pay)
{ payload = pl;
gross_weight = gw;
mpg = in_mpg;
hourly_pay = pay; };
float cost_per_full_day(float cost_of_gas) {
return(8.0 * hourly_pay +
8.0 * cost_of_gas * 55.0 / mpg); };
};
main()
{
driven_truck chuck_ford;
chuck_ford.initialize_all(20000.0, 12000.0, 5.2, 12.50);
cout << "The efficiency of the Ford is " <<
chuck_ford.efficiency() << "\n";
cout << "The cost per mile for Chuck to drive is " <<
chuck_ford.cost_per_mile() << "\n";
cout << "The cost of Chuck driving the Ford for a day is " <<
chuck_ford.cost_per_full_day(1.129) << "\n";
}
// Result of execution
//
// The efficiency of the Ford is .625
// The cost per mile for Chuck to drive is 0.227273
// The cost of Chuck driving the Ford for a day is 195.530762
DUPLICATED METHOD NAMES
You will notice that both of the parent classes have a method named initialize(), and both of these are inherited into the subclass with no difficulty. However, if we attempt to send a message to one of these methods, we will have a problem, because the system does not know which we are referring to. This problem will be solved and illustrated in the next example program.
Before going on to the next example program, it should be noted that we have not declared any objects of the two parent classes in the main program. Since the two parent classes are simply normal classes themselves, it should be apparent that there is nothing magic about them and they can be used to define and manipulate objects in the usual fashion. You may wish to do this to review your knowledge of simple classes and objects of those classes.
Be sure to compile and execute this program after you understand its operation completely.
MORE DUPLICATE METHOD NAMES
The second example program in this chapter named MULTINH2.CPP, illustrates the use of classes with duplicate method names being inherited into a derived class.
If you study the code, you will find that a new method has been added to all three of the classes named cost_per_full_day(). This was done intentionally to illustrate how the same method name can be used in all three classes. The class definitions are no problem at all, the methods are simply named and defined as shown. The problem comes when we wish to use one of the methods since they are all the same name and they have the same numbers and types of parameters and identical return types. This prevents some sort of an overloading rule to disambiguate the message sent to one or more of the methods.
The method used to disambiguate the method calls are illustrated in lines 60, 64, and 68 of the main program. The solution is to prepend the class name to the method name with the double colon as used in the method implementation definition. This is referred to as qualifying the method name. Qualification is not necessary in line 68 since it is the method in the derived class and it will take precedence over the other method names. Actually, you could qualify all method calls, but if the names are unique, the compiler can do it for you and make your code easier to write and read.
Be sure to compile and execute this program and study the results. The observant student will notice that there is a slight discrepancy in the results given in lines 79 through 81, since the first two values do not add up to the third value exactly. This is due to the limited precision of the float variable but should cause no real problem.
MULTINH2.CPP
// Chapter 9 - Program 2
#include <iostream.h>
class moving_van {
protected:
float payload;
float gross_weight;
float mpg;
public:
void initialize(float pl, float gw, float in_mpg) {
payload = pl;
gross_weight = gw;
mpg = in_mpg; };
float efficiency(void) {
return(payload / (payload + gross_weight)); };
float cost_per_ton(float fuel_cost) {
return(fuel_cost / (payload / 2000.0)); };
float cost_per_full_day(float cost_of_gas) {
return(8.0 * cost_of_gas * 55.0 / mpg); };
};
class driver {
protected:
float hourly_pay;
public:
void initialize(float pay) {hourly_pay = pay; };
float cost_per_mile(void) {return(hourly_pay / 55.0); } ;
float cost_per_full_day(float overtime_premium) {
return(8.0 * hourly_pay); };
};
class driven_truck : public moving_van, public driver {
public:
void initialize_all(float pl, float gw, float in_mpg, float pay)
{ payload = pl;
gross_weight = gw;
mpg = in_mpg;
hourly_pay = pay; };
float cost_per_full_day(float cost_of_gas) {
return(8.0 * hourly_pay +
8.0 * cost_of_gas * 55.0 / mpg); };
};
main()
{
driven_truck chuck_ford;
chuck_ford.initialize_all(20000.0, 12000.0, 5.2, 12.50);
cout << "The efficiency of the Ford is " <<
chuck_ford.efficiency() << "\n";
cout << "The cost per mile for Chuck to drive is " <<
chuck_ford.cost_per_mile() << "\n";
cout << "The cost per day for the Ford is " <<
chuck_ford.moving_van::cost_per_full_day(1.129) <<
"\n";
cout << "The cost of Chuck for a full day is " <<
chuck_ford.driver::cost_per_full_day(15.75) <<
"\n";
cout << "The cost of Chuck driving the Ford for a day is " <<
chuck_ford.driven_truck::cost_per_full_day(1.129) <<
"\n";
}
// Result of execution
//
// The efficiency of the Ford is .625
// The cost per mile for Chuck to drive is 0.227273
// The cost per day for the Ford is 95.530769
// The cost of Chuck for a full day is 100.0
// The cost of Chuck driving the Ford for a day is 195.530762
DUPLICATED VARIABLE NAMES
If you will examine the example program named MULTINH3.CPP, you will notice that each base class has a variable with the same name.
According to the rules of inheritance, an object of the driven_truck class will have two variables with the same name, weight. This would be a problem if it weren't for the fact that C++ has defined a method of accessing each one in a well defined way. You have probably guessed that we will use qualification to access each variable. Lines 38 and 45 illustrate the use of the variables. It may be obvious, but it should be explicitly stated, that there is no reason that the subclass itself cannot have a variable of the same name as those inherited from the parent classes. In order to access it, no qualification would be required.
It should be apparent to you that once you understand single inheritance, multiple inheritance is nothing more than an extension of the same rules. Of course, if you inherit two methods or variables of the same name, you must use qualification to allow the compiler to select the correct one.
MULTINH3.CPP
// Chapter 9 - Program 3
#include <iostream.h>
class moving_van {
protected:
float payload;
float weight;
float mpg;
public:
void initialize(float pl, float gw, float in_mpg) {
payload = pl;
weight = gw;
mpg = in_mpg; };
float efficiency(void) {
return(payload / (payload + weight)); };
float cost_per_ton(float fuel_cost) {
return(fuel_cost / (payload / 2000.0)); };
};
class driver {
protected:
float hourly_pay;
float weight;
public:
void initialize(float pay, float in_weight) {
hourly_pay = pay;
weight = in_weight; };
float cost_per_mile(void) {return(hourly_pay / 55.0); } ;
float drivers_weight(void) {return(weight); };
};
class driven_truck : public moving_van, public driver {
public:
void initialize_all(float pl, float gw, float in_mpg, float pay)
{ payload = pl;
moving_van::weight = gw;
mpg = in_mpg;
hourly_pay = pay; };
float cost_per_full_day(float cost_of_gas) {
return(8.0 * hourly_pay +
8.0 * cost_of_gas * 55.0 / mpg); };
float total_weight(void) {
return(moving_van::weight + driver::weight); };
};
main()
{
driven_truck chuck_ford;
chuck_ford.initialize_all(20000.0, 12000.0, 5.2, 12.50);
chuck_ford.driver::initialize(15.50, 250.0);
cout << "The efficiency of the Ford is " <<
chuck_ford.efficiency() << "\n";
cout << "The cost per mile for Chuck to drive is " <<
chuck_ford.cost_per_mile() << "\n";
cout << "The cost of Chuck driving the Ford for a day is " <<
chuck_ford.cost_per_full_day(1.129) << "\n";
cout << "The total weight is " << chuck_ford.total_weight() <<
"\n";
}
// Result of execution
//
// The efficiency of the Ford is .625
// The cost per mile for Chuck to drive is 0.227273
// The cost of Chuck driving the Ford for a day is 195.530762
// The total weight is 12250
PRACTICAL MULTIPLE INHERITANCE
Examine the example program named DATETIME.H for a practical example using multiple inheritance. You will notice that we are returning to our familiar date and time classes from earlier chapters.
There is a good deal to be learned from this very short header file since it is our first example of member initialization. There are two constructors for this class, the first being a very simple constructor that does nothing in itself as is evident from an examination of line 12. This constuctor allows the constructors to be executed for the classes new_date and time_of_day. In both cases a constructor will be executed that requires no parameters, and such a constructor is available for each of these two classes.
The second constuctor is more interesting since it does not simply use the default constructor, but instead passes some of the input parameters to the inherited class constructors. Following the colon in line 13 are two member initializers which are used to initialize members of this class. Since the two parent classes are inherited, they are also members of this class and can be initialized as shown. Each of the member initializers is actually a call to a constructor of the parent classes and it should be evident that there must be a constructor with the proper number of input parameters to respond to the messages given. You will note that in line 14, we are actually calling the constructor with no parameters given explicitly. If we chose, we could simply let the system call that constructor automatically, but this gives us an explicit comment on what is happening.
DATETIME.H
// Chapter 9 - Program 4
#ifndef DATETIME_H
#define DATETIME_H
#include "newdate.h"
#include "time.h"
class datetime : public new_date, public time_of_day {
public:
datetime(void) { ; }; // Default to todays date and time
datetime(int M, int D, int Y, int H, int Mn, int S) :
new_date(), // Member initializer
time_of_day(H, Mn, S) // Member initializer
{ set_date(M, D, Y); }; // Constructor body
};
#endif
MORE ABOUT MEMBER INITIALIZERS
Actually, we can use the member initializer to initialize class members also. If we had a class member of type int named member_var, we could initialize it also by mentioning the name of the member followed by the value we desired to initialize it to in parentheses. If we wished to initialize it to the value 13, we could use the following line of code in the member initializer list;
member_var(13);
Following all member initialization, the normal constructor code is executed which in this case is given in line 16.
ORDER OF MEMBER INITIALIZATION
The order of member initialization may seem a bit strange, but it does follow a few simple rules. The order of member initialization does not follow the order given by the initialization list, but another very strict order over which you have complete control. All inherited classes are initialized first in the order they are listed in the class header. If lines 14 and 15 were reversed, class new_date would still be initialized first because it is mentioned first in line 8. It has been mentioned that C++ respects its elders and initializes its parents prior to itself. That should be a useful memory aid in the use of member initializers.
Next, all local class members are initialized in the order in which they are declared in the class, not the order in which they are declared in the initialization list. Actually, it would probably be good practice to not use the member initializer to initialize class members but instead to initialize them in the normal constructor code.
Finally, after the member initializers are all executed in the proper order, the main body of the constructor is executed in the normal manner.
USING THE NEW CLASS
The example program named USEDTTM.CPP uses the datetime class we just built, and like our previous examples, the main program is kept very simple and straight forward. You will note that the default constructor is used for the object named now, and the constructor with the member initializers is used with the objects named birthday and special.
The diligent student should have no trouble understanding the remaining code in this example.
USEDTTM.CPP
// Chapter 9 - Program 5
#include <iostream.h>
#include "datetime.h"
datetime now, birthday(10, 18, 1938, 1, 30, 17);
datetime special( 2, 19, 1950, 13, 30, 0);
void main(void)
{
cout << "Now = " << now.get_date_string() << " "
<< now.get_time_string() << " and is day "
<< now.get_day_of_year() << "\n";
cout << "Birthday = " << birthday.get_date_string() << " "
<< birthday.get_time_string() << " and is day "
<< birthday.get_day_of_year() << "\n";
cout << "Special = " << special.get_date_string() << " "
<< special.get_time_string() << " and is day "
<< special.get_day_of_year() << "\n";
}
// Result of execution
// Now = Jan 20, 1992 21:12:56 and is day 20
// Birthday = Oct 18, 1938 1:30:17 and is day 291
// Special = Feb 19, 1950 13:30:00 and is day 50
FUTURE DIRECTIONS OF C++
An ANSI committee has been formed to write an ANSI standard for C++. They first met in the Spring of 1990 and are expected to complete the standard in about three years. Until the new standard is released, the C++ language is expected to stay fairly stable. However, due to the nature of compiler writers and their desire to slightly improve their offerings over their competitors, you can bet that the language will not remain static during this three year period.
Many small changes have been added during the past year that barely affect the casual programmer, or even the heavy user of the language. You can be sure that the language will evolve slowly and surely into a very usable and reliable language. There are two areas, however, that should be discussed in a little detail because they will add so much to the language in future years. Those two topics are parameterized types and exception handling.
FUTURE DIRECTIONS - PARAMETERIZED TYPES
Many times, when developing a program, you wish to perform some operation on more than one data type. For example you may wish to sort a list of integers, another list of floating point numbers, and a list of alphabetic strings. It seems silly to have to write a separate sort function for each of the three types when all three are sorted in the same logical way. With parameterized types, you will be able to write a single sort routine that is capable of sorting all three of the lists.
This is already available in the Ada language as the generic package or procedure. Because it is available in Ada, there is a software components industry that provides programmers with prewritten and thoroughly debugged software routines that work with many different types. When this is generally available in C++, there will be a components industry for C++ and precoded, debugged and efficient source code will be available off the shelf to perform many of the standard operations. These operations will include such things as sorts, queues, stacks, lists, etc.
Bjarne Stroustrup has announced that parameterized types, otherwise known as templates or generics, will be available in a future version of C++. He has presented a paper with details of one way to implement them, but this is only a suggestion, not a specification.
Borland International has included templates in version 3.0 of Borland C++, and hopefully their implementation will be very close to the final definition of templates.
The next three example programs will illustrate the use of templates with Borland's compiler, but may not work with other compilers.
THE FIRST TEMPLATE
The example program named TEMPLAT1.CPP is the first example of the use of a template. This program is so simple it seems silly to even bother with it but it will illustrate the use of the parameterized type.
The template is given in lines 4 through 8 with the first line indicating that it is a template with a single type to be replaced, the type ANY_TYPE. This type can be replaced by any type which can be used in the comparison operation in line 7. If you have defined a class, and you have overloaded the operator ">", then this template can be used with objects of your class. Thus, you do not have to write a maximum function for each type or class in your program.
This function is included automatically for each type it is called with in the program, and the code itself should be very easy to understand.
The diligent student should realize that nearly the same effect can be achieved through use of a macro, except that when a macro is used, the strict type checking is not done. Because of this and because of the availability of the inline method capability in C++, the use of macros is essentially non-existent by experienced C++ programmers.
TEMPLAT1.CPP
// Chapter 9 - Program 6
#include <stdio.h>
template<class ANY_TYPE>
ANY_TYPE maximum(ANY_TYPE a, ANY_TYPE b)
{
return (a > b) ? a : b;
}
void main(void)
{
int x = 12, y = -7;
float real = 3.1415;
char ch = 'A';
printf("%8d\n", maximum(x, y));
printf("%8d\n", maximum(-34, y));
printf("%8.3f\n", maximum(real, float(y)));
printf("%8.3f\n", maximum(real, float(x)));
printf("%c\n", maximum(ch, 'X'));
}
// Result of execution
// 12
// -7
// 3.141
// 12.000
// X
A CLASS TEMPLATE
The example program named TEMPLAT2.CPP is a little more involved since it provides a template for an entire class rather than a single function. The template code is given in lines 6 through 16 and a little study will show that this is an entire class definition. The diligent student will recognize that this is a very weak stack class since there is nothing to prevent popping data from an empty stack, and there is no indication of a full stack. Our intent, however, is to illustrate the use of the parameterized type and to do so using the simplest class possible.
In the main program we create an object named int_stack in line 25 which will be a stack designed to store integers, and another object named float_stack in line 26 which is designed to store float type values. In both cases, we enclose the type we desire this object to work with in "<>" brackets, and the system creates the object by first replacing all instances of ANY_TYPE with the desired type, then creating the object of that type. You will note that any type can be used that has an assignment capability since lines 13 and 14 use the assignment operator on the parameterized type.
Even though the strings are all of differing lengths, we can even use the stack to store a stack of strings if we only store a pointer to the strings and not the entire string. This is illustrated in the object named string_stack declared in line 27 and used later in the program.
This program should be fairly easy for you to follow if you spend a bit of time studying it. You should compile and run it if you have a compiler that will handle this new construct.
TEMPLAT2.CPP
// Chapter 9 - Program 7
#include <stdio.h>
const int MAXSIZE = 128;
template<class ANY_TYPE>
class stack
{
ANY_TYPE array[MAXSIZE];
int stack_pointer;
public:
stack(void) { stack_pointer = 0; };
void push(ANY_TYPE in_dat) { array[stack_pointer++] = in_dat; };
ANY_TYPE pop(void) { return array[--stack_pointer]; };
int empty(void) { return (stack_pointer == 0); };
}
char name[] = "John Herkimer Doe";
void main(void)
{
int x = 12, y = -7;
float real = 3.1415;
stack<int> int_stack;
stack<float> float_stack;
stack<char *> string_stack;
int_stack.push(x);
int_stack.push(y);
int_stack.push(77);
float_stack.push(real);
float_stack.push(-12.345);
float_stack.push(100.01);
string_stack.push("This is line 1");
string_stack.push("This is the second line");
string_stack.push("This is the third line");
string_stack.push(name);
printf("Integer stack ---> ");
printf("%8d ", int_stack.pop());
printf("%8d ", int_stack.pop());
printf("%8d\n", int_stack.pop());
printf(" Float stack ---> ");
printf("%8.3f ", float_stack.pop());
printf("%8.3f ", float_stack.pop());
printf("%8.3f\n", float_stack.pop());
printf("\n Strings\n");
do {
printf("%s\n", string_stack.pop());
} while (!string_stack.empty());
}
// Result of execution
// Integer stack ---> 12 -7 77
// Float stack ---> 3.141 -12.345 100.010
//
// Strings
// John Herkimer Doe
// This is the third line
// This is the second line
// This is line 1
REUSING THE STACK CLASS
The program named TEMPLAT3.CPP uses the same class with the template as defined in the last program but in this case, it uses the date class developed earlier as the stack members. More specifically, it uses a pointer to the date class as the stack member.
Because class assignment is legal, you could also store the actual class in the stack rather than just the pointer to it. To do so however, would be very inefficient since the entire class would be copied into the stack each time it is pushed and the entire class would be copied out again when it was popped. Use of the pointer is a little more general, so it was illustrated here for your benefit.
All three of the previous programs can be compiled and executed if you have a copy of Borland C++ version 3.0. Other compilers may not work with these programs since parameterized types are not yet a part of the C++ specification.
TEMPLAT3.CPP
// Chapter 9 - Program 8
#include <stdio.h>
#include "date.h"
const int MAXSIZE = 128;
template<class ANY_TYPE>
class stack
{
ANY_TYPE array[MAXSIZE];
int stack_pointer;
public:
stack(void) { stack_pointer = 0; };
void push(ANY_TYPE in_dat) { array[stack_pointer++] = in_dat; };
ANY_TYPE pop(void) { return array[--stack_pointer]; };
int empty(void) { return (stack_pointer == 0); };
}
char name[] = "John Herkimer Doe";
void main(void)
{
stack<char *> string_stack;
stack<date *> class_stack;
date cow, pig, dog, extra;
class_stack.push(&cow);
class_stack.push(&pig);
class_stack.push(&dog);
class_stack.push(&extra);
string_stack.push("This is line 1");
string_stack.push("This is the second line");
string_stack.push("This is the third line");
string_stack.push(name);
for (int index = 0 ; index < 5 ; index++) {
extra = *class_stack.pop();
printf("Date = %d %d %d\n", extra.get_month(),
extra.get_day(), extra.get_year());
};
printf("\n Strings\n");
do {
printf("%s\n", string_stack.pop());
} while (!string_stack.empty());
}
// Result of execution
// Date = 1 7 1992
// Date = 1 7 1992
// Date = 1 7 1992
// Date = 1 7 1992
//
// Strings
// John Herkimer Doe
// This is the third line
// This is the second line
// This is line 1
FUTURE DIRECTIONS - EXCEPTION HANDLING
A future version of C++ will have some form of exception handling to allow the programmer to trap errors and prevent the system from completely shutting down when a fatal error occurs. The Ada language allows the programmer to trap any error that occurs, even system errors, execute some recovery code, and continue on with the program execution in a very well defined way. Bjarne Stroustrup, working in conjunction with the ANSI-C++ committee, has announced that some form of exception handling will be implemented but he has not stated what form it would take as of this writing.
WHAT SHOULD BE YOUR NEXT STEP?
Once again, we have reached a major milestone in C++ programming. With the ability to use inheritance, you have nearly all of the tools you need to effectively use the object oriented programming techniques of C++ and you would do well to stop studying again and begin programming. The only topic left with C++ is virtual methods which are used for dynamic binding or polymorphism. This will be covered in the next two chapters. The vast majority of all programming can be done without dynamic binding, and in attempting to force it into every program, you could wind up with an unreadable mess, so you should approach it slowly.