Multiple Inheritance in C++ is a powerful yet intricate feature that allows a class to inherit properties and behaviors from more than one base class. In traditional single inheritance, a class can have only one immediate parent, but with multiple inheritance, a derived class can inherit from multiple base classes. This flexibility provides developers with the capability to create complex class hierarchies and share features from different sources. However, it also introduces challenges related to ambiguity resolution and the potential for the diamond problem.
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 compile able with any good quality C++ compiler provided it follows C++ 11 or newer version.
Table of Contents
- Multiple Inheritance in Java and Python
- Multiple Inheritance in C++
- Simple C++ Multiple Inheritance Example
- Practical Multiple Inheritance
- Duplicated Method Names
- Duplicated Variable Names
- Member Initializers
- Parameterized Types
- Exception Handling
Multiple Inheritance in Java and Python
In object-oriented programming, languages like Java and Python take distinct approaches to inheritance. Java, for instance, supports single inheritance only, while Python allows both single and multiple inheritance. C++ stands out by supporting both and gives developers a unique range of choices when designing class structures. Understanding how multiple inheritance is handled in C++ as opposed to other languages is crucial for effective utilization of this feature and address its associated complexities.
Multiple Inheritance in C++
C++ Language’s most powerful feature is multiple inheritance which makes it more powerful than Java. It is possible for one class to inherit the attributes of two or more classes.
The general form is class DerivedClass : BaseClassList //seperated with commas { };
Here is a simple example.
1 2 3 4 5 6 7 8 9 10 | class X{ int a; }; class Y{ int b; }; class Z : public X, public Y { int c; }; |
Here Z class has the access to both the members of X & Y class.
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 C++ Multiple Inheritance Example
Let’s consider a scenario where we have two base classes, Shape
and Color
, and we want to create a derived class ColoredShape
that inherits from both. This simple example illustrates the basic syntax of multiple inheritance in C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <iostream> class Shape { public: void draw() { std::cout << "Drawing Shape\n"; } }; class Color { public: void setColor(const std::string& c) { color = c; } std::string getColor() const { return color; } private: std::string color; }; class ColoredShape : public Shape, public Color { // Additional functionality can be added here }; int main() { ColoredShape coloredShape; coloredShape.draw(); coloredShape.setColor("Red"); std::cout << "Color: " << coloredShape.getColor() << '\n'; return 0; } |
Practical Multiple Inheritance
Consider a scenario where you have a class DatabaseConnection
handling database connectivity and another class Logger
managing logging functionality. Creating a class DatabaseLogger
through multiple inheritance allows you to seamlessly integrate both functionalities into a single class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | #include <iostream> #include <string> class DatabaseConnection { public: void connect() { std::cout << "Connected to the database\n"; } void disconnect() { std::cout << "Disconnected from the database\n"; } void executeQuery(const std::string& query) { std::cout << "Executing query: " << query << '\n'; // Perform the database query execution logic } }; class Logger { public: void log(const std::string& message) { std::cout << "Log: " << message << '\n'; } void logError(const std::string& errorMessage) { std::cerr << "Error: " << errorMessage << '\n'; } }; class DatabaseLogger : public DatabaseConnection, public Logger { public: void performDatabaseOperation(const std::string& query) { connect(); executeQuery(query); disconnect(); log("Database operation completed"); } void performSecureDatabaseOperation(const std::string& query, const std::string& username, const std::string& password) { // Perform authentication logic if (authenticate(username, password)) { connect(); executeQuery(query); disconnect(); log("Secure database operation completed"); } else { logError("Authentication failed"); } } private: bool authenticate(const std::string& username, const std::string& password) { // Implement authentication logic, for example, checking credentials return (username == "admin" && password == "admin123"); } }; int main() { DatabaseLogger dbLogger; dbLogger.performDatabaseOperation("SELECT * FROM Users"); std::cout << "\n"; dbLogger.performSecureDatabaseOperation("UPDATE Users SET Password='newpass'", "admin", "admin123"); // Attempting an incorrect authentication to demonstrate error logging dbLogger.performSecureDatabaseOperation("UPDATE Users SET Password='newpass'", "user", "wrongpassword"); return 0; } |
In this example, the DatabaseLogger
class includes the performDatabaseOperation
method, which shows the sequence of connecting to the database, executing a query, disconnecting, and logging the completion. Additionally, the performSecureDatabaseOperation
method demonstrates a more complex scenario by incorporating authentication logic before executing a database operation. The authenticate
method checks whether the provided username and password match a predefined set of credentials. This shows how multiple inheritance allows us to seamlessly integrate functionalities from both the base classes.
Duplicated Method Names
Take a look at the following C++ Program. 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.
Be sure to compile and execute this program after you understand its operation completely.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | #include <iostream> class MovingVan { 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() const { return payload / (payload + gross_weight); } float cost_per_ton(float fuel_cost) const { return fuel_cost / (payload / 2000.0); } float cost_per_full_day(float cost_of_gas) const { 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() const { return hourly_pay / 55.0; } float cost_per_full_day(float overtime_premium) const { return 8.0 * hourly_pay; } }; class DrivenTruck : public MovingVan, 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) const { return 8.0 * hourly_pay + 8.0 * cost_of_gas * 55.0 / mpg; } }; int main() { DrivenTruck chuck_ford; chuck_ford.initialize_all(20000.0, 12000.0, 5.2, 12.50); std::cout << "The efficiency of the Ford is " << chuck_ford.efficiency() << "\n"; std::cout << "The cost per mile for Chuck to drive is " << chuck_ford.cost_per_mile() << "\n"; std::cout << "The cost per day for the Ford is " << chuck_ford.MovingVan::cost_per_full_day(1.129) << "\n"; std::cout << "The cost of Chuck for a full day is " << chuck_ford.Driver::cost_per_full_day(15.75) << "\n"; std::cout << "The cost of Chuck driving the Ford for a day is " << chuck_ford.cost_per_full_day(1.129) << "\n"; return 0; } |
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.
Duplicated Variable Names
Now examine the following C++ Program, you will notice that each base class has a variable with the same name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | #include <iostream> class MovingVan { 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() const { return payload / (payload + weight); } float cost_per_ton(float fuel_cost) const { return fuel_cost / (payload / 2000.0); } }; class Driver { protected: float hourly_pay; float driver_weight; public: void initialize(float pay, float in_weight) { hourly_pay = pay; driver_weight = in_weight; } float cost_per_mile() const { return hourly_pay / 55.0; } float drivers_weight() const { return driver_weight; } }; class DrivenTruck : public MovingVan, public Driver { public: void initialize_all(float pl, float gw, float in_mpg, float pay) { payload = pl; MovingVan::weight = gw; mpg = in_mpg; hourly_pay = pay; } float cost_per_full_day(float cost_of_gas) const { return 8.0 * hourly_pay + 8.0 * cost_of_gas * 55.0 / mpg; } float total_weight() const { return MovingVan::weight + driver_weight; } }; int main() { DrivenTruck chuck_ford; chuck_ford.initialize_all(20000.0, 12000.0, 5.2, 12.50); chuck_ford.Driver::initialize(15.50, 250.0); std::cout << "The efficiency of the Ford is " << chuck_ford.efficiency() << "\n"; std::cout << "The cost per mile for Chuck to drive is " << chuck_ford.cost_per_mile() << "\n"; std::cout << "The cost of Chuck driving the Ford for a day is " << chuck_ford.cost_per_full_day(1.129) << "\n"; std::cout << "The total weight is " << chuck_ford.total_weight() << "\n"; return 0; } |
Following the principles of inheritance, a driven_truck object inherently has two variables named weight. While this might pose an issue, C++ offers a well-defined approach to access each variable. Lines 38 and 45 in the code exemplify the application of these variables and requires qualification for distinct identification. It is crucial to note that the subclass itself can have a variable sharing the same name as those inherited from parent classes, eliminating the need for qualification when accessing it.
Understanding single inheritance lays the foundation for comprehending multiple inheritance as merely an extension of the same principles. If two inherited methods or variables share identical names, qualification becomes imperative to guide the compiler in selecting the appropriate one.
Member Initializers
In C++, we can utilize member initializers to conveniently initialize class members. For instance, if we have a class member named member_var
of type int
, we can set its initial value by specifying the member’s name followed by the desired value in parentheses. For instance, to initialize it to 13, we would use the following line of code in the member initializer list:
1 | member_var(13); |
After all member initialization is complete, the regular constructor code is executed, as seen in line 16.
Order of Member Initializers
While the order of member initialization might appear peculiar, it adheres to simple rules. The order is not dictated by the initialization list but follows a strict sequence within your control. Inherited classes are initialized first, listed in the order specified in the class header. Even if lines 14 and 15 were reversed, new_date
would still be initialized first because it’s mentioned first in line 8. This follows the principle that C++ respects its elders and initializes its parents before itself—a helpful memory aid for working with member initializers.
Subsequently, local class members are initialized in the order of declaration within the class, not according to the initialization list. While using member initializers for class members is possible, it’s often considered good practice to initialize them in the regular constructor code.
After executing all member initializers in the proper order, the main body of the constructor proceeds in the usual manner.
Parameterized Types
In programming, there are often situations where you need to perform a operation on different data types. For instance, you might want to sort lists of integers, floating-point numbers, and strings in a similar manner. It becomes impractical to write separate sorting functions for each type when the logic is essentially the same. Parameterized types, also known as templates or generics, offer a solution by enabling the creation of a single sorting routine capable of handling multiple data types concurrently.
While this capability is already present in the Ada language through the use of generic packages or procedures, its absence in C++ has limited the availability of prewritten, thoroughly debugged software routines that work with various types. Bjarne Stroustrup, the creator of C++, has announced that parameterized types will be introduced in a future version of C++, opening the door for a components industry to provide readily available, pre-coded, and efficient source code for standard operations like sorting, queues, stacks, and lists.
The Template Example
In the following C++ we introduce the concept of templates with a simple example. The template, starting from line 4, defines a function called maximum
that can compare and return the greater of two values of any data type. This parameterized type, denoted by ANY_TYPE
, allows flexibility in handling various data types without writing separate functions for each.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #include <stdio.h> template<class ANY_TYPE> ANY_TYPE maximum(ANY_TYPE a, ANY_TYPE b) { return (a > b) ? a : b; } int 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')); return 0; } |
The template allows for concise and reusable code, handling different data types effortlessly. The diligent student should recognize that this approach is more versatile than using macros, which lack strict type checking.
A Class Template
In the following program, we delve deeper into templates by creating a class template named stack
. This template, from line 6 to 16, defines a simple stack class capable of storing various data types. The main program demonstrates the usage of this template by creating instances of the stack for integers, floats, and strings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #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"; int main(void) { int x = 12, y = -7; float real = 3.1415; stack<int> int_stack; stack<float> float_stack; stack<char *> string_stack; // ... (rest of the program) return 0; } |
This class template demonstrates the flexibility of parameterized types, enabling the creation of a stack for various data types. The diligent student will recognize the simplicity of the class and understand its primary purpose: to illustrate the use of parameterized types. The code, though basic, serves as a fundamental example for novice C++ programmers.
Exception Handling
The C++ language now supports exception handling. This valuable feature allows programmers to intercept errors and prevent abrupt program termination when encountering fatal errors. Bjarne Stroustrup, working in collaboration with the ANSI-C++ committee, has ensured the incorporation of exception handling into the language. The support for exception handling is available in modern versions of C++, enhancing the robustness of error management and providing more reliable program execution.