dcsimg
 

Understanding the Intricacies of Multiple Inheritance in C++

by Manoj Debnath
Understanding the Intricacies of Multiple Inheritance in C++

Understanding the Intricacies of Multiple Inheritance in C++

Introduction

Multiple inheritance basically means a class derived from more than one base classes. This is an efficient class design to reuse the properties of multiple classes into a single composite class. The syntax in implementing one in the hierarchy of inheritance is quite simple. But, there are ambiguities and strange situations that may occur down the hierarchy. This is the simplest reason why pure object-oriented languages such as Java or Smalltalk disallow class design with multiple inheritance completely. C++, however, retained the idea with many traits of the object-oriented model for the sake of the advantage it provides, although with a caution of careful design. This article covers some traits associated with multiple inheritance in C++ with appropriate examples.

A Hybrid Language

C++ is termed as a hybrid language due to its support for multiple languages paradigm, be it the object-oriented principle of Smalltalk or procedural legacy of C. Unlike its predecessor Smalltalk, a pure object-oriented programming language, C++ inherits many of its object-oriented characteristics. It also includes functional features, low-level memory manipulation technique, and is a superset of C. Pure object-oriented programming languages like Smalltalk or Java support class hierarchy as a single monolithic tree, so the question of multiple inheritance does not arise with them. However, C++ allows multiple distinct inheritance trees. Therefore, the idea of multiple inheritance as a logical fortification of combining of more than one base class can be derived into a single class. Now, the question is do programmers really need multiple inheritance? This is a debatable proposition because there are a lot of arguments both in favor of and against this feature. But, the important fact is, unlike Java, there are no restrictions imposed by the language and C++ is open to both the arguments. The choice, however, lies with the programmer who can happily design their classes according to their like.

Use of Multiple Inheritance

It is easy to create a flexible class design using multiple inheritance. For example, we can combine the benefits of a class created by two separate entities and derive a new class. Notably, the template basic_iostream class is a derivation of two templates, namely basic_istream and basic_ostream, in the standard C++ library. The operators overloaded in the base classes provide a convenient notation for performing both input and output. For example, the left-shift operator (<<) overloaded in the basic_ostream is designated for stream output and the right-shift (>>) operator, overloaded in basic_istream, is designated for stream input. These operators are used with the standard stream objects cin, cout, cerr, and clog. Therefore, basic_iostream combines the benefit of both the input and output operation into a single entity.

Difficulties with Multiple Inheritance

Multiple inheritance is a powerful capability to encourage interesting forms of software reuse but it cannot be denied that it also poses a variety of ambiguity problems. The problems are often so subtle that modern programming languages like Java or C# completely removed the idea from their programming paradigm.

A very common problem associated with multiple inheritance is that the base classes may have data members or member functions with a common name. This is a situation of clear ambiguity during compilation. Consider the following example.

#include <iostream>
#include <string>

using namespace std;

class A{
protected:
   int intVal;
public:
   A(int val):intVal(val){
   }
   int getInfo() const{
      return intVal;
   }
};

class B{
protected:
   string strVal;
public:
   B(string val):strVal(val){
   }
   string getInfo() const{
      return strVal;
   }
};

class Derived_A_B: public A, public B{
private:
   double dblVal;
protected:
   friend ostream &operator<<(ostream &,
      const Derived_A_B &);
public:
   Derived_A_B(int, string, double);
   double getDblVal() const;
};

Derived_A_B::Derived_A_B(int i, string s, double d)
   :A(i),B(s),dblVal(d){
}
double Derived_A_B::getDblVal() const{
   return dblVal;
}

ostream &operator<<(ostream &out,
      const Derived_A_B &derived){
   out<<" Integer value:"<<derived.intVal
      <<"\n String value:"<<derived.strVal
      <<"\n Double value:"<<derived.dblVal;
   return out;
}

int main()
{
   A a(100);
   B b("Hello");
   Derived_A_B d(111,"Hi",2.5);

   cout<<"Instance of A contains integer:"
      <<a.getInfo()<<endl;
   cout<<"Instance of B contains string:"
      <<b.getInfo()<<endl;
   cout<<"Instance of Derived_A_B contains:"
      <<d<<endl;

   cout<<"Instance of A contains integer:"
      <<d.A::getInfo()<<endl;
   cout<<"Instance of B contains string:"
      <<d.B::getInfo()<<endl;
   cout<<"Instance of Derived_A_B contains:"
      <<d.getDblVal()<<endl;

   A *ptrA = &d;
   cout<<"ptrA getInfo() returns:"
      <<ptrA->getInfo()<<endl;
   B *ptrB = &d;
   cout<<"ptrB getInfo() returns:"
      <<ptrB->getInfo()<<endl;

   return 0;
}

Observe that the Derived_A_B class explicitly calls base class constructors for each of its base classes in the order of their inheritance. If the constructors were not called explicitly, the default constructors would be called implicitly. In main, the instances of the base classes A and B, and the derived class instance are initialized appropriately. The call to the method getInfo() of the base class by the derived class object is a clear call of ambiguity and compiler does not allow that.

// Ambiguity
cout<<"Instance of Derived_A_B contains:"
   <<d.getInfo()<<endl;

This ambiguity is due to the fact that the derived class object contains two getInfo() functions: one inherited from class A and another from class B. However, we can resolve the problem by using the scope resolution operator as follows.

cout<<"Instance of A contains integer:"
   <<d.A::getInfo()<<endl;
cout<<"Instance of B contains string:"
   <<d.B::getInfo()<<endl;

The multiple inheritance that follows the diamond inheritance structure must either be avoided during class design or it must be carefully designed. The diamond inheritance structure is shown in Figure 1.

The diamond inheritance structure
Figure 1: The diamond inheritance structure

The example structure is a classic example of this potentially problematic model yet carefully designed structure found in the standard C++ library. The classes basic_istream and basic_ostream inherit from basic_ios. basic_iostream can contain two copies of the members of class basic_ios, one inherited via basic_istream and another via basic_ostream. Had it not been carefully designed, it would result in a compilation error.

Using the Virtual Base Class to Overcome Ambiguity

Another way to solve the problem of inheriting duplicate copies of inherited base classes is by using a virtual base class. First, let us see where the problem lies.

#include <iostream>
using namespace std;

class ios_demo{
public:
   virtual void display() const = 0;
};

class istream_demo: public ios_demo {
public:
   void display() const {
      cout<<"from basic_istream_demo"<<endl;
   }
};

class ostream_demo: public ios_demo {
public:
   void display() const {
      cout<<"from basic_ostream_demo"<<endl;
   }
};

class iostream_demo:
      public istream_demo,
      public ostream_demo{
public:
   void display() const {
      cout<<"from basic_iostream_demo"<<endl;
      cout<<"--";
      ostream_demo::display();
      cout<<"--";
      istream_demo::display();
   }
};

int main()
{
   ostream_demo a;
   istream_demo b;
   iostream_demo ab;

   ios_demo *arr[3];
   arr[0] = &a;
   arr[1] = &b;
   arr[2] = &ab;   // ERROR: not allowed, ambiguous

   for(int i=0;i<3;i++){
      arr[i]->display();
   }
   return 0;
}

Here we create the diamond inheritance structure where ostream_demo and istream_demo each are derived from ios_demo. The derived class iostream_demo inherits from both istream_demo and ostream_demo. As a result, in main, when we try to assign the object of iostream_demo into the array designated as ios_demo, it creates an ambiguous problem of duplicate sub-object and is not allowed by the compiler.

arr[2] = &ab;   // ERROR: not allowed, ambiguous

We can, however, eliminate the problem by using virtual base class inheritance. In this way, the class is inherited as virtual, which means only one copy of the sub object will be inherited by the derived class. Other parts of the code will remain the same; the ambiguity is simply removed.

class ios_demo{
// ...
};

class istream_demo: virtual public ios_demo {
// ...
};

class ostream_demo: virtual public ios_demo {
// ...
};

class iostream_demo: {
// ...
};

int main()
{

// ...
   ios_demo *arr[3];
   arr[0] = &a;
   arr[1] = &b;
   arr[2] = &ab;   // OK now

// ...
   return 0;
}

Conclusion

Now, having a glimpse of the issues associated with multiple inheritance in C++, we must consider that if we need to use public interfaces of more than one base classes through a derived class, we may need to use multiple inheritance. Also, if we need to do up-casting to base classes, we may consider designing multiple inheritance. Therefore, multiple inheritance should be avoided if we can and use it only when necessary. The beauty of C++ is that it imposes no restriction but the appeal is to be cautious in using many of its powerful features.

This article was originally published on Tuesday Mar 24th 2020
Home
Mobile Site | Full Site