This particular page contains materials from the second of the two chapters that introduce the C++ class concept. This second chapter, "Intermediate class" covers a number of topics including operator functions, friend relations, resource management, and an introduction to inheritance. The content material here comes from the introducing inheritance.
Independent classes? That isn't always the case.
In some circumstances, in the analysis phase or in the early stages of the design phase you will identify similarities among the prototype classes that you have proposed for your program. Often, exploitation of such similarities leads to an improved design, and sometimes can lead to significant savings in implementation effort.
You have used "Draw" programs so you know the kind of interface that such a program would have. There would be a "palette of tools" that a user could use to add components. The components would include text (paragraphs describing the circuit), and circuit elements like the batteries and light bulbs. The editor part would allow the user to select a component, move it onto the main work area and then, by doubly clicking the mouse button, open a dialog window that would allow editing of text and setting parameters such as a resistance in ohms. Obviously, the program would have to let the user save a partially designed circuit to a file from where it could be restored later.
What objects might the program contain?
The objects are all pretty obvious (at least they are obvious once you've been playing this game long enough). The following are among the more important:
During a preliminary design process your group would be right to come up with classes Battery, Document, Palette, Resistor, Switch. Each group member could work on refining one or two classes leading to an initial set of descriptions like the following:
class TextParagraph Owns: a block of text and a rectangle defining position in main view (window). Does: GetText() - uses a standard text editing dialog to get text changed; FollowMouse() - responds to middle mouse button by following mouse to reposition within view; DisplayText() - draws itself in view; Rect() - returns bounding rectangle; ... Save() and Restore() - transfers text and position details to/.from file. class Battery Owns: Position in view, resistance (internal resistance), electromotive force, possibly a text string for some label/name, unique identifier, identifiers of connecting wires... Does: GetVoltStuff() - uses a dialog to get voltage, internal resistance etc. TrackMouse() - respond to middle mouse button by following mouse to reposition within view; DrawBat() - draws itself in view; AddWire() - add a connecting wire; Area() - returns rectangle occupied by battery in display view; ... Put() and Get() - transfers parameters to/from file. class Resistor Owns: Position in view, resistance, possibly a text string for some label/name, unique identifier, identifiers of connecting wires... Does: GetResistance() - uses a dialog to get resistance, label etc. Move() - respond to middle mouse button by following mouse to reposition within view; Display() - draws itself in view; Place() - returns area when resistor gets drawn; ... ReadFrom() and WriteTo() - transfers parameters to/from file.
You should be able to sketch out pseudo code for some of the main operations. For example, the document's function to save data to a file might be something like the following:
Document::DoSave write paragraphList.Length() iterator i1(paragraphList) for i1.First(), !i1.IsDone() do paragraph_ptr = i1.CurrentItem(); paragraph_ptr->Save() i1.Next(); write BatteriesList.Length() iterator i2(BatteriesList) for i2.First, !i2.IsDone() do battery_ptr = i2.CurrentItem() battery_ptr->Put() ...The function to display all the data of the document would be rather similar:
Document::Draw iterator i1(paragraphList) for i1.First(), !i1.IsDone() do paragraph_ptr = i1.CurrentItem(); paragraph_ptr->DisplayText() i1.Next(); iterator i2(BatteriesList) for i2.First, !i2.IsDone() do battery_ptr = i2.CurrentItem() battery_ptr->DrawBat() ...Another function of "Document" would sort out which data element was being picked when the user wanted to move something using the mouse pointer:
Document::LetUserMoveSomething(Point mousePoint) iterator i1(paragraphList) Paragraph *pp = NULL; for i1.First(), !i1.IsDone() do paragraph_ptr = i1.CurrentItem(); Rectangle r = paragraph_ptr->Rect() if(r.Contains(mousePoint) pp = paragraph_ptr; i1.Next(); if(pp != NULL) pp->FollowMouse() return iterator i2(BatteriesList) battery *pb for i2.First, !i2.IsDone() do battery_ptr = i2.CurrentItem() Rectangle r = battery_ptr ->Area() if(r.Contains(mousePoint) pb = battery_ptr ; i2.Next(); if(pb != NULL) pb->TrackMouse() return ...
By now you should have the feeling that there is something amiss. The design with its "batteries", "wires", "text paragraphs" seems sensible. But the code is coming out curiously clumsy and unattractive in its inconsistencies.
Batteries, switches, wires, and text paragraphs may be wildly different kinds of things, but from the perspective of "document" they actually have some similarities. They are all "things" that perform similar tasks. A document can ask a "thing" to:
Some "things" are more similar than others. Batteries, switches, and resistors will all have specific roles to play in the circuit simulation, and there will be many similarities in their roles. Wires are also considered in the circuit simulation, but their role is quite different, they just connect active components. Text paragraphs don't get involved in the circuit simulation part. So all of them are "storable, drawable, editable" things, some are "circuit things", and some are "circuit things that have resistances".
You can represent such relationships among classes graphically, as illustrated in Figure 23.5. As shown there, there is a kind of hierarchy.
Class Thing captures just the concept of some kind of data element that can draw itself, save itself to file and so forth. There are no data elements defined for Thing, it is purely conceptual, purely abstract.
A TextParagraph is a particular kind of Thing. A TextParagraph does own data, it owns its text, its position and so forth. You can also define actual code specifying exactly how a TextParagraph might carry out specific tasks like saving itself to file. Whereas class Thing is purely conceptual, a TextParagraph is something pretty real, pretty "concrete". You can "see" a TextParagraph as an actual data structure in a running program.
In contrast, a CircuitThing is somewhat abstract. You can define some properties of a CircuitThing. All circuit elements seem to need unique identifiers, they need coordinate data defining their position, and they need a character string for a name or a label. You can even define some of the code associated with CircuitThings - for instance, you could define functions that access coordinate data.
Wires are special kinds of CircuitThings. It is easy to define them completely. They have a few more data fields (e.g. identifiers of the components that they join, or maybe coordinates for their endpoints). It is also easy to define completely how they perform all the functions like saving their data to file or drawing themselves.
Components are a different specialization of CircuitThing. Components are CircuitThings that will have to be analyzed by the circuit simulation component of the program. So they will have data attributes like "resistance", and they may have many additional forms of behaviour as required in the simulation.
Naturally, Battery, Switch, and Resistor define different specializations of this idea of Component. Each will have its unique additional data attributes. Each can define a real implementation for functions like Draw().
One thing that you immediately gain is consistency. In the original design sketch, text paragraphs, batteries and so forth all had some way of defining that these data elements could display themselves, save themselves to file and so forth. But each class was slightly different; thus we had TextParagraph::Save(), Battery::Put() and Resistor:: WriteTo(). The hierarchy allows us to capture the concept of "storability" by specifying in class Thing the ability WriteTo(). While each specialization performs WriteTo() in a unique way, they can at least be consistent in their names for this common behaviour. But consistency of naming is just a beginning.
If you exploit such similarities, you can greatly simplify the design of the overall application as can be seen by re-examining some of the tasks that a Document must perform.
While you might want separate lists of the various specialized Components (as this might be necessary for the circuit simulation code), you could change Document so that it stores data using a single thingList instead of separate paragraphList, BatteriesList and so forth . This would allow simplification of functions like DoSave():
Document::DoSave(...) write thingList.Length() iterator i1(thingList) for i1.First(), !i1.IsDone() do thing_ptr = i1.CurrentItem(); thing_ptr->WriteTo() i1.Next(); Document::Draw iterator i1(thingList) for i1.First(), !i1.IsDone() do thing_ptr = i1.CurrentItem(); thing_ptr->Draw() i1.Next(); Document::LetUserMoveSomething(Point mousePoint) iterator i1(thingList) Thing *pt = NULL; for i1.First(), !i1.IsDone() do thing_ptr = i1.CurrentItem(); Rectangle r = thing_ptr ->Area() if(r.Contains(mousePoint) pt = thing_ptr ; i1.Next(); if(pt != NULL) pt->TrackMouse() returnThe code is no longer obscured by all the different special cases. The revised code is shorter and much more intelligible.
Note also how the revised Document no longer needs to know about the different kinds of circuit component. This would prove useful later if you decided to have another component (e.g. class Voltmeter); you wouldn't need to change the code of Document in order to accommodate this extension.
The most significant benefit is the resulting simplification of design, and simultaneous acquisition of extendability. But you may gain more. Sometimes, you can define the code for a particular behaviour at the level of a partially abstract class. Thus, you should be able to define the access function for getting a CircuitThing's identifier at the level of class CircuitThing while class Component can define the code for accessing a Component's electrical resistance. Defining these functions at the level of the partially abstract classes saves you from writing very similar functions for each of the concrete classes like Battery, Resistor, etc.
You start by defining the "base class", in this case that is class Thing which is the base class for the entire hierarchy:
class Thing { public: virtual ~Thing() { } /* Disk I/O */ virtual void ReadFrom(istream& i s) = 0; virtual void WriteTo(ostream& os) const = 0; /* Graphics */ virtual void Draw() const = 0; /* mouse interactions */ virtual void DoDialog() = 0; // For double click virtual void TrackMouse() = 0; // Mouse select and drag virtual Rect Area() const = 0; ... };Class Thing represents just an idea of a storable, drawable data element and so naturally i t is simply a list of function names.
The situation is a little odd. We know that all Things can draw themselves, but we can't say how. The ability to draw is common, but the mechanism depends very much on the specialized nature of the Thing that is asked to draw itself. In class Thing, we have to be able to say "all Things respond to a Draw() request, specialized Thing subclasses define how they do this".
This is what the keyword virtual and the odd = 0 notation are for.
Roughly, the keyword virtual identifies a function that a class wants to define in such a way that subclasses may later extend or otherwise modify the definition. The =0 part means that we aren't prepared to offer even a default implementation. (Such undefined virtual functions are called "pure virtual functions".)
In the case of class Thing, we can't provide default definitions for any of the functions like Draw(), WriteTo() and so forth. The implementations of these functions vary too much between different subclasses. This represents an extreme case; often you can provide a default implementation for a virtual function. This default definition describes what "usually" should be done. Subclasses that need to something different can replace, or "override", the default definition.
The destructor, ~Thing(), does have a definition: virtual ~Thing() { }. The definition is an empty function; basically, it says that by default there is no tidying up to be done when a Thing is deleted. The destructor is virtual. Subclasses of class Thing may be resource managers (e.g. a subclass might allocate space for an object label as a separate character array in the heap). Such specialized Things will need destructors that do some cleaning up.
A C++ compiler prevents you from having variables of type Thing:
Thing aThing; // illegal, Thing is an abstractionThis is of course appropriate. You can't have Things. You can only have instances of specialized subclasses. (This is standard whenever you have a classification hierarchy with abstract classes. After all, you never see "mammals" walking around, instead you encounter dogs, cats, humans, and horses - i.e. instances of specialized subclasses of class mammal). However, you can have variables that are Thing* pointers, and you can define functions that take Thing& reference arguments:
Thing *first_thing;The pointer first_thing can hold the address of (i.e. point to) an instance of class TextParagraph, or it might point to a Wire object, or point to a Battery object.
Once you have declared class Thing, you can declare classes that are "based on" or "derived from" this class:
class TextParagraph : public Thing { TextParagraph(Point topleft); virtual ~TextParagraph(); /* Disk I/O */ virtual void ReadFrom(istream& is); virtual void WriteTo(ostream& os) const; /* Graphics */ virtual void Draw() const; /* mouse interactions */ virtual void DoDialog(); // For double click virtual void TrackMouse(); // Mouse select and drag virtual Rect Area() const; // Member functions that are unique to TextParagraphs void EditText(); ... private: // Data needed by a TextParagraph Point fTopLeft; char *fText; ... }; class CircuitThing : public Thing { CircuitThing(int ident, Point where); virtual ~CircuitThing(); ... /* Disk I/O */ virtual void ReadFrom(istream& is); virtual void WriteTo(ostream& os) const; ... // Additional member functions that define behaviours // characteristic of all kinds of CircuitThing int GetId() const { return this->fId } virtual Rect Area() const { return Rect( this->flocation.x - 8, this->flocation.y - 8, this->flocation.x + 8, this->flocation.y + 8); } virtual double Current() const = 0; ...Protected access specifier
protected: // Data needed by a CircuitThing int fId; Point flocation; char *fLabel; ... };
In later studies you will learn that there are a variety of different ways that "derivation" can be used to build up class hierarchies. Initially, only one form is important. The important form is "public derivation". Both TextParagraph and CircuitThing are "publicly derived" from class Thing:
class TextParagraph : public Thing { ... }; class CircuitThing : public Thing { ... };
We need actual TextParagraph objects. This class has to be "concrete". The class declaration has to be complete, and all the member functions will have to be defined. Naturally, the class declaration starts with the constructor(s) and destructor. Then it will have to repeat the declarations from class Thing; so we again get functions like Draw() being declared. This time they don't have those = 0 definitions. There will have to be definitions provided for each of the functions. (It is not actually necessary to repeat the keyword virtual; this keyword need only appear in the class that introduces the member function. However, it is usually simplest just to "copy and paste" the block of function declarations and so have the keyword.) Class TextParagraph will introduce some additional member functions describing those behaviours that are unique to TextParagraphs. Some of these additional functions will be in the public interface; most would be private. Class TextParagraph would also declare all the private data members needed to record the data possessed by a TextParagraph object.
Class CircuitThing is an in between case. It is not a pure abstraction like Thing, nor yet is it a concrete class like TextParagraph. Its main role is to introduce those member functions needed to specify the behaviours of all different kinds of CircuitThing and to describe those data members that are possessed by all kinds of CircuitThing.
Class CircuitThing cannot provide definitions for all of those pure virtual functions inherited from class Thing; for instance it can't do much about Draw(). It should not repeat the declarations of those functions for which it can't give a definition. Virtual functions only get re-declared in those subclasses where they are finally defined.
Class CircuitThing can specify some of the processing that must be done when a CircuitThing gets written to or read from a file on disk. Obviously, it cannot specify everything; each specialized subclass has its own data to save. But CircuitThing can define how to deal with the common data like the identifier, location and label:
void CircuitThing::WriteTo(ostream& os) const { // keyword virtual not repeated in definition os << fId << endl; os << fLocation.x << " " << fLocation.y << endl; os << fLabel << endl; } void CircuitThing::ReadFrom(istream& is) { is >> fId; is >> fLocation.x >> fLocation.y; char buff[256]; is.getline(buff,255,'\n'); delete [] fLabel; // get rid of existing label fLabel = new char[strlen(buff) + 1]; strcpy(fLabel, buff); }These member functions can be used by the more elaborate WriteTo() and ReadFrom() functions that will get defined in subclasses. (Note the deletion of fLabel and allocation of a new array; this is another of those places where it is easy to get a memory leak.)
The example illustrates that there are three possibilities for additional member functions:
int GetId() const { return this->fId } virtual Rect Area() const { return Rect( this->flocation.x - 8, this->flocation.y - 8, this->flocation.x + 8, this->flocation.y + 8); } virtual double Current() const = 0;
Function GetId() is not a virtual function. Class CircuitThing defines an implementation (return the fId identifier field). Because the function is not virtual, subclasses of CircuitThing cannot change this implementation. You use this style when you know that there is only one reasonable implementation. for a member function.
Function Area() has a definition. It creates a rectangle of size 16x16 centred around the fLocation point that defines the centre of a CircuitThing. This might suit most specialized kinds of CircuitThing; so, to economise on coding, this default implementation can be defined at this level in the hierarchy. Of course, Area() is still a virtual function because that was how it was specified when first introduced in class Thing ("Once a virtual function, always a virtual function"). Some subclasses, e.g. class Wire, might need different definitions of Area(); they can override this default definition by providing their own replacement.
Function Current() is an additional pure virtual function. The circuit simulation code will require all circuit elements know the current that they have flowing. But the way this gets calculated would be class specific.
Class CircuitThing declares some of the data members - fId, fLabel, and fLocation. There is a potential difficulty with these data members.
These data members should not be public; you don't want the data being accessed from anywhere in the program. But if the data members are declared as private, they really are private, they will only be accessible from the code of class CircuitThing itself. But you can see that the various specialized subclasses are going to have legitimate reasons for wanting to use these variables. For example, all the different versions of Draw() are going to need to know where the object is located in order to do the correct drawing operations.
You can't use the "friend" mechanism to partially relax the security. When you define class CircuitThing you won't generally know what the subclasses will be so you can't nominate them as friends.
There has to be a mechanism to prevent external access but allow access by subclasses- so there is. There is a third level of security on members. In addition to public and private, you can declare data members and member functions as being protected. A protected member is not accessible from the main program code but can be accessed in the member functions of the class declaring that member, or in the member functions of any derived subclass.
Here, variables like fLocation should be defined as protected. Subclasses can then use the fLocation data in their Draw() and other functions. (Actually, it is sometimes better to keep the data members private and provide extra protected access functions that allow subclasses to get and set the values of these data members. This technique can help when debugging complex programs involving elaborate class hierarchies).
Once the definition of class CircuitThing is complete, you have to continue with its derived classes: class Wire, and class Component:
class Wire : public CircuitThing { public: Wire(int startcomponent, int endcomponent, Point p1, Point p2); ~Wire();Class Wire is meant to be a concrete class; the program will use instances of this class. So it has to define all member functions.Thing declared behaviours
/* Disk I/O */ virtual void ReadFrom(istream& is); virtual void WriteTo(ostream& os) const; /* Graphics */ virtual void Draw() const; /* mouse interactions */ virtual void DoDialog(); // For double click virtual void TrackMouse(); // Mouse select and drag virtual Rect Area() const;CircuitThing behaviours
virtual double Current() const; ...Own unique behaviours
int FirstEndId() { return this->fFirstEnd; } ... private: int fFirstEnd; ... };
The class repeats the declarations for all those virtual functions, declared in classes from which it is derived, for which it wants to provide definitions (or to change existing definitions). Thus class Wire will declare the functions like Draw() and Current(). Class Wire also declares the ReadFrom() and WriteTo() functions as these have to be redefined to accommodate additional data, and it declares Area() as it wants to use a different size.
Class Wire would also define additional member functions characterising its unique behaviours and would add some data members. The extra data members might be declared as private or protected. You would declare them as private if you knew that no-one was ever going to try to invent subclasses based on your class Wire. If you wanted to allow for the possibility of specialized kinds of Wire, you would make these extra data members (and functions) protected. You would then also have to define the destructor as virtual.
The specification of the problem might disallow the user from dragging a wire or clicking on a wire to open a dialog box. This would be easily dealt with by making the Area() function of a Wire return a zero sized rectangle (rather than the fixed 16x16 rectangle used by other CircuitThings):
Rect Wire::Area() const { return Rect(0, 0, 0, 0); }(The program identifies the Thing being selected by testing whether the mouse was located in the Thing's area; so if a Thing's area is zero, it can never be selected.) This definition of Area() overrides that provided by CircuitThing.
A Wire has to save all the standard CircuitThing data to file, and then save its extra data. This can be done by having a Wire::WriteTo() function that makes use of the inherited function:
void Wire::WriteTo(ostream& os) { CircuitThing::WriteTo(os); os << fFirstEnd << " " << fSecondEnd << endl; ... }This provides another illustration of how inheritance structures may lead to small savings of code. All the specialized subclasses of CircuitThing use its code to save the identifier, label, and location.
void Document::DoSave(ostream& out) { out << thingList.Length() << endl; iterator i1(thingList); i1.First(); while(!i1.IsDone()) { Thing* thing_ptr = (Thing*) i1.CurrentItem(); thing_ptr ->WriteTo(out); i1.Next(); } }
The code generated for
thing_ptr ->WriteTo()isn't supposed to invoke function Thing::WriteTo(). After all, this function doesn't exist (it was defined as = 0). Instead the code is supposed to invoke the appropriate specialized version of WriteTo().
But which is the appropriate function? That is going to depend on the contents of thingList. The thingList will contain pointers to instances of class TextParagraph, class Battery, class Switch and so forth. These will be all mixed together in whatever order the user happened to have added them to the Document. So the appropriate function might be Battery::WriteTo() for the first Thing in the list, Resistor::WriteTo() for the second list element, and Wire::WriteTo() for the third. You can't know until you are writing the list at run-time.
The compiler can't work things out at compile time and generate the instruction sequence for a normal subroutine call. Instead, it has to generate code that works out the correct routine to use at run time.
The generated code makes use of tables that contain the addresses of functions. There is a table for each class that uses virtual functions; a class's table contains the addresses of its (virtual) member functions. The table for class Wire would, for example, contain pointers to the locations in the code segment of each of the functions Wire::ReadFrom(), Wire::WriteTo(), Wire::Draw() and so forth. Similarly, the virtual table for class Battery will have the addresses of the functions Battery::ReadFrom() and so on. (These tables are known as "virtual tables".)
In addition to its declared data members, an object that is an instance of a class that uses virtual functions will have an extra pointer data member. This pointer data member holds the address of the virtual table that has the addresses of the functions that are to be used in association with that object. Thus every Wire object has a pointer to the Wire virtual table, and every Battery object has a pointer to the Battery virtual table. A simple version of the scheme is illustrated in Figure 23.6
The instruction sequence generated for something like:
thing_ptr ->WriteTo()involves first using the link from the object pointed to by thing_ptr to get the location of the table describing the functions. Then, the required function, WriteTo(), is "looked up" in this table to find where it is in memory. Finally, a subroutine call is made to the actual WriteTo() function. Although it may sound complex, the process requires only three or four instructions!
Function lookup at run time is referred to as "dynamic binding". The address of the function that is to be called is determined ("bound") while the program is running (hence "dynamically"). Normal function calls just use the machine's JSR (jump to subroutine) instruction with the function's address filled in by the compiler or linking loader. Since this is done before the program is running, the normal mechanism of fixing addresses for subroutine calls is said to use static binding (the address is fixed, bound, before the program is moving, or while it is static).
It is this "dynamic binding" that makes possible the simplification of program design. Things like Document don't have to have code to handle each special case. Instead the code for Document is general, but the effect achieved is to invoke different special case functions as required.
Another term that you will find used in relation to these programming styles is "polymorphism". This is just an anglicisation of two Greek words - poly meaning many, and morph meaning shape. A Document owns a list of Things; Things have many different shapes - some are text paragraphs, others are wires. A pointer like thing_ptr is a "polymorphic" pointer in that the thing it points to may, at different times, have different shapes.
Multiple inheritance introduces all sorts of complexities. Most uses of multiple inheritance are inappropriate for beginners. There is only one form usage that you should even consider.
Multiple inheritance can be used as a "type composition" device. This is just a systematic generalization of the previous example where we had class Thing that represented the type "a drawable, storable, editable data item occupying an area of a window".
Instead of having class Thing as a base class with all these properties, we could instead factor them into separate classes:
class Storable { public: virtual ~Storable() { } virtual void WriteTo(ostream&) const = 0; virtual void ReadFrom(istream&) const = 0; ... }; void Drawable { public: virtual ~Drawable() { } virtual void Draw() const = 0; virtual Rect Area() const = 0; ... };This allows "mix and match". Different specialized subclasses can derive from chosen base classes. As a TextParagraph is to be both storable and drawable, it can inherit from both base classes:
class TextParagraph : public Storable, public Drawable { ... };You might have another class, Decoration, that provides some pretty outline or shadow effect for a drawable item. You don't want to store Decoration objects in a file, they only get used while the program is running. So, the Decoration class only inherits from Drawable:
class Decoration : public Drawable { ... };
As additional examples, consider class Printable and class Comparable:
class Printable { public: virtual ~Printable() { } virtual void PrintOn(ostream& out) const = 0; }; ostream& operator << (ostream& o, const Printable& p) { p.PrintOn(o); return o; } ostream& operator << (ostream& o, const Printable *p_ptr) { p_ptr->PrintOn(o); return o; } class Comparable { public: virtual ~Comparable() { } virtual int Compare(const Comparable* ptr) const = 0; int Compare(const Comparable& other) const { return Compare(&other); } int operator==(const Comparable& other) const { return Compare(other) == 0; } int operator!=(const Comparable& other) const { return Compare(other) != 0; } int operator < (const Comparable& other) const { return Compare(other) < 0; } int operator <= (const Comparable& other) const { return Compare(other) <= 0; } int operator>(const Comparable& other) const { return Compare(other) > 0; } int operator>=(const Comparable& other) const { return Compare(other) >= 0; } };Class Printable packages the idea of a class with a PrintOn() function and associated global operator << () functions. Class Comparable characterizes data items that compare themselves with similar data items. It declares a Compare() function that is a little like strcmp(); it should return -1 if the first item is smaller than the second, zero if they are equal, and 1 if the first is greater. The class also defines a set of operator functions, like the "not equals function" operator !=() and the "greater than" function operator>(); all involve calls to the pure virtual Compare() function with suitable tests on the result code. (The next chapter has some example Compare() functions.)
As noted earlier, another possible pure virtual base class would be class Iterator:
class Iterator { public: virtual ~Iterator() { } virtual void First(void) = 0; virtual void Next(void) = 0; virtual int IsDone(void) const = 0; virtual void *CurrentItem(void) const = 0; };This would allow the creation of a hierarchy of iterator classes for different kinds of data collection. Each would inherit from class Iterator.
Now inventing classes like Storable, Comparable, and Drawable is not a task for beginners. You need lots of experience before you can identify widely useful abstract concepts like the concept of storability. However you may get to work with library code that has such general abstractions defined and so you may want to define classes using multiple inheritance to combine different data types.
What do you gain from such use of inheritance as a type composition device?
Obviously, it doesn't save you any coding effort. The abstract classes from which you multiply inherit are exactly that - abstract. They have no data members. All, or most, of their member functions are pure virtual functions with no definitions. If any member functions are defined, then as in the case of class Comparable, these definitions simply provide alternative interfaces to one of the pure virtual functions.
You inherit, but the inheritance is empty. You have to define the code.
The advantage is not for the implementor of a subclass. Those who benefit are the maintenance programmers and the designers of the overall system. They gain because if a project uses such abstract classes, the code becomes more consistent, and easier to understand. The maintenance programmer knows that any class whose instances are to be stored to file will use the standard functions ReadFrom() and WriteTo(). The designer may be able to simplify the design by using collections of different kinds of objects as was done with the Document example.
But these are all advanced, difficult features.
The important uses of inheritance are those illustrated - capturing commonalities to simplify design, and using (multiple) inheritance as a type composition device. These uses will be illustrated in later examples. Most of Part V of this text is devoted to simple uses of inheritance.