OO Design with Multiple Inheritance Multiple Inheritance in C++ C++ allows a class to inherit implementation code from multiple superclasses. inheritance (MI). This is referred to as multiple Some programmers like multiple inheritance because real-world entities (such as yourself) do inherit traits from multiple sources. Many programmers shun MI because it can introduce deeply hidden bugs into a large program and can make it more daunting to extend a class hierarchy. 1
Any design that uses multiple inheritance can be converted into one in which a class inherits implementation code from only one superclass. Java does not allow a class to inherit implementation code from multiple superclasses. However, Java does allow a class to inherit behaviors from multiple interfaces. 2
Some Simple Examples for MI Let s say we wish to use classes to represent the following roles in an educational system: Student Teacher TeachingAssistant Student Teacher ^ ^ -------- ------ TeachingAssistant 3
GenericVehicle PeopleHaulerTraits FreightHaulerTraits ^ ^ ^ ^ \ --------------/---------- \ / \ \ / \ \ / \ \ / \ PassengerVehicle FreightHauler PersonalTransporterTraits CommercialTransporterTraits ^ ^ ^ \ / / / / \ / / \ / / \ / ------ / \ / \ / \ / \ / \ / \ / Car Bus Truck FreightLiner 4
While the above two examples lend themselves straightforwardly to multiple inheritance, let s now consider an example where the decision to use multiple inheritance may be dictated by some basic tenet of object-oriented programming, such as keeping different data abstractions as loosely coupled as possible. 5
Let s say we want to model the various ways in which a set of mechanical widgets can be assembled in a factory. (We may want to do so to carry out a cost benefit analysis of the different methods for assembly.) 6
The assembly operations may be carried out robotically, manually, or semiautomatically using different systems. If parts are assembled from random initial positions in a work area, automatic and semiautomatic assembly would need some sort of a computer vision module for localizing the parts before they can be assembled. After the parts are localized, the computer would also have to calculate the motion trajectories to use for mating one part with another part. For that, it would need to know the initial and the final pose of each part. We will assume that the computer has available to it full 3D geometric models of the parts for such path planning calculations. 7
We obviously have the following three issues to deal with here and, for program organization, it is best to think of them separately: 1. Specifying the assembly operations at a purely abstract level. 2. The choice of the agent that would actually carry out the assembly again at an abstract level. The agent could be a robot, a human, or some semiautomatic system. 3. A geometry engine for computing the motion trajectories to be used for mating one part with another part when assembly is carried out robotically. Such motion trajectories may also be needed for some types of semiautomatic assembly. For manual assembly, the calculated motion trajectories may help us determine the level of dexterity expected of a human worker. 8
The first issue capturing at an abstract level the assembly operations needed could be addressed by defining an Assemble class as shown in the next slide. This class uses two ancillary classes: Part for representing the parts to be assembled, and Pose to represent the location and the orientation of each part in space. The class Part presumably has at least a data member that points to a geometric model of the part. Such models would be needed by a geometry engine to figure out the collision-free trajectories for assembling one part with another. 9
class Assemble { protected: Part* part1; // part1 to be assembled with part2 // part2 assumed fixtured Part* part2; Pose* part1_initial_pose; Pose* part1_final_pose; Pose* pose_part2; bool done; public: Assemble( Part1* p1, Part* p2, Pose* s1_init, Pose* s1_final, Pose* s2, done = false ); virtual void grasppart() {}; virtual void orientpart() {}; virtual void pickuppart() {}; virtual void insert() {}; virtual bool isassemblydone() { return done; } // the rest of the class virtual ~Assemble(); }; 10
The names of the member functions speak for themselves. As a data abstraction, the class Assemble stands on its own, independent of the physical mechanism used for assembly. The simplistic implementations provided for the functions are supposed to take care of the requirement that when a function is declared to be virtual, it must be defined at the same time. Obviously, their override definitions in the subclasses of Assemble would be more useful. 11
For addressing the second of the three issues outlined previously, we can now extend the Assemble class and provide more meaningful implementations for its various member functions depending on the specific assembly agent used. 12
class AssembleWithRobot : public Assemble { // stuff related to robot calibration // and the coordinate transformation from // the world frame into a robot end-effector // based coordinate frame public: AssembleWithRobot( Part1* part1, Part* part2, Pose* part1_init_pose, Pose* part1_final_pose, Pose* s2, done = false ); void grasppart(); void orientpart(); void pickuppart(); void insert(); // the rest of the class }; class AssembleSemiAutomatically : public Assemble { /*... */ }; class AssembleManually : public Assemble { /*... */ };... 13
The functions such as grasppart(), orientpart(), and so on, for the robotic and semiautomatic assembly would take into account the kinematic and dynamic constraints of the machines involved, but again at a purely abstract level. 14
Now we can write a function that, through polymorphism, could be used to perform assemblies: void assemble( Assemble* agent ) { agent->grasppart(); // grasp part1 agent->insert(); // insert part1 into part2 //... if ( agent->done() ) { // assembly finished, start next step } else { //... } //... } The important thing to note here is that the assemble() function is independent of the kind of Assemble object that we may actually be using. Polymorphism would guarantee us that, inside assemble(), the correct function is invoked for each Assemble. 15
This brings us to the issue of how to actually do geometry calculations for figuring out the motion trajectories needed for taking part1 from its initial pose, as given by the value of the data member part1 initial pose of the class Assemble, to its final pose, as given by the data member part1 final pose. The function insert() defined for Assemble would simply not work unless it has access to some kind of a geometry engine for path planning. The question now is: How do we incorporate the path planning facilities offered by a vendor-supplied geometry engine in the Assemble class hierarchy? 16
One option is to declare the GeometryEngine class as a base for the Assemble class: class Assemble : public GeometryEngine { protected: Part* part1; Part* part2; Pose* part1_initial_pose; Pose* part1_final_pose; Pose* pose_part2; bool done; public: Assemble( Part1* p1, Part* p2, Pose* s1_init, Pose* s1_final, Pose* s2, done = false ); virtual void grasppart(); virtual void orientpart(); virtual void pickuppart(); virtual void insert(); virtual bool isassemblydone() { return done; } // the rest of the class virtual ~Assemble(); }; 17
The path planning functions inherited from the GeometryEngine class would be overridden in each subclass of Assemble class to take into account the special constraints of the assembly agent corresponding to that class. Graphically, our class hierarchy for Assemble and its extensions would look like: GeometryEngine Assemble AssembleWithRobot AssembleSemiAutomatically AssembleManually AssembleWithSystem1 AssembleWithSystem2 18
While this design could be made to serve its intended function, it violates a basic tenet of good OO programming: Data abstractions that are conceptually separate and distinct should be kept as uncoupled as possible As originally conceived, the data abstraction represented by the class Assemble was complete unto itself and distinct from the path planning implementation code packaged in the GeometryEngine class. But, by making Assemble a subclass of GeometryEngine, we have destroyed the separate identity of Assemble. 19
Now we will show a different design in which we do not violate the separateness of the abstractions. In this new design, we will specify Assemble as a pure interface, meaning an abstract class with no implementation code: class Assemble { public: virtual void grasppart() = 0; virtual void orientpart() = 0; virtual void pickuppart() = 0; virtual void insert() = 0; virtual bool isassemblydone() = 0; // the rest of the class virtual ~Assemble() {}; }; Now that all the functions of Assemble are pure virtual, we do not have to provide them with the simplistic implementations that we had to in our previous design. Being an abstract class, our new Assemble class does not need a constructor. We have also included a virtual destructor that can be used for cleaning up the data to be defined in the derived classes. 20
Now the definition of AssembleWithRobot might look like: }; class AssembleWithRobot : public Assemble, protected GeometryEngine { Part* part1; Part* part2; Pose* part1_initial_pose; Pose* part1_final_pose; Pose* pose_part2; bool done; protected: // code for overriding any virtual functions of // GeometryEngine class public: AssembleWithRobot( Part1* p1, Part* p2, Pose* s1_init, Pose* s1_final, Pose* s2, done = false ); virtual void grasppart(); virtual void orientpart(); virtual void pickuppart(); virtual void insert(); virtual bool isassemblydone(); ~ AssembleWithRobot(); 21
Here we have multiple inheritance. In this particular implementation of MI, the nature of inheritance from the two bases of AssembleWithRobot is different. The public derivation from the base class Assemble will allow us to use polymorphism with respect to the virtual functions declared in that base class. On the other hand, the protected derivation from GeometryEngine will allow AssembleWithRobot and its subclasses to inherit the path planning implementation code in that base. With this construction, we are evidently making a design decision that we do not need polymorphism with respect to the path planning functions in GeometryEngine. It goes without saying that AssembleWithRobot class is required to provide implementations for all the abstract functions declared in the base Assemble. 22
The other derived classes in the Assemble hierarchy can now be defined as follows: class AssembleSemiAutomatically : public Assemble, protected GeometryEngine { /*... */ }; class AssembleManually : public Assemble, protected GeometryEngine { /*...*/ };... 23
GeometryEngine Assemble AssembleWithRobot AssembleSemiAutomatically AssembleManually AssembleWithSystem1 AssembleWithSystem2 protected derivation public derivation Graphically, the entire hierarchy can be shown as here. 24
The two approaches to the design we have presented are not the only ones available. Another possibility would be to use GeometryEngine* as a data member inside Assemble. That could be made to work, provided that GeometryEngine has no virtual member functions that would need to be overridden in Assemble and its subclasses. In any case, the MI-based design appears more natural and more logical to the situation at hand, and it meets a design criterion that it is best to keep distinct abstractions separate. Nonetheless, it is worthwhile to point out that the implementation code at the level of concrete classes such as AssembleWithRobot will remain substantially the same no matter which approach is used. 25
Issues that Arise with Repeated Inheritance The most complicating issues that arise with MI have to do with what is known as repeated inheritance. Repeated inheritance takes place when a derived class inherits the same members of some base class through two different paths in a class hierarchy. 26
Employee address name getname print setname Manager department level getlevel print setlevel SalesPerson department salestarget print setsalestarget setterritory SalesManager 27
The classes Manager and SalesPerson are both derived from the base class Employee. And the class SalesManager is derived from both Manager and SalesPerson. We have intentionally left unspecified the data members and the member functions in the final derived-class SalesManager, as the following discussion bears directly on their specification. 28
When a derived class can inherit members through multiple paths and when the different paths have an upstream class in common, the following issues become immediately relevant. 29
1. The Problem of Duplicate Construction of Common-Base Subobject: Recall from earlier discussion that when the constructor of a derived class is invoked, the derived-class object so constructed has built inside it a base-class subobject. Base class slice of the derived-class object Derived-class object 30
That implies that, unless precautions are taken, when a constructor for SalesManager is invoked, we would end up with two different versions of the Employee slice in the constructed object. Employee Employee Manager SalesPerson SalesManager How do we prevent the formation of duplicate common-base subobjects in a derived-class object? 31
2. The Name-Conflict Problem for Member Functions: Suppose two member functions of the same signature but different implementations are inherited by the SalesManager class from the two different inheritance paths shown. If these member functions are not overridden in the SalesManager class, we can end up with an ill-formed program. This could, for example, be the case with the print() member function that is listed originally as a member of the class Employee. Let s say this function is modified by each class between Employee and SalesManager. If for some reason, this modified function is not overridden in SalesManager but yet invoked on an object of type SalesManager, you would have a compile-time ambiguity. 32
3. The Name-Conflict Problem for Data Members: The class SalesManager inherits two different data members with the same name department one each from the two superclasses Manager and SalesPerson. These two data members, although possessing the same name, possess different meanings for the derived class SalesManager because a SalesManager could conceivably have two different department attributes associated with him or her. Such an individual could belong to a particular department of the corporation and, at the same time, be in charge of a particular unit of the sales organization which could also be referred to as a department. So how does one make sure that despite the same name department each data member gets the correct value when we construct an object of type SalesManager? 33