C++ Techniques

This section will look at specific C++ programming techniques, useful classes and problems (and, hopefully, solutions) that developers encounter.

Ian Horwill begins a series that examines C++ features from a beginner's point of view and in three related articles, The Harpist, Ulrich Eisenecker and Roger Lever look at object relationships and how multiple inheritance may or may not fit in.

Wait for me! - copying and assignment

by Ian Horwill

Awaiting copyright clearance.

Related objects

by The Harpist

I sent Francis an article to forward to Sean Corfield about some uses of multiple inheritance. He read it and returned it with the suggestion that it was really the tag end of a much larger topic. So here is the first of what I intend to be a two part article on the way objects are related and can be used as components.

In the beginning there were the built-in types inherited from C. There were also a number of derived types, pointers, arrays (perhaps more correctly, vectors) and structs. C and C++ added type qualifiers - one each, C++ added const and then C added volatile. In C, type qualifiers were just that and nothing more. They represented two simple concepts, read only access and unreliable memory (memory that could change at the most inconvenient moment by intervention from outside the program).

C++ added references

None of this would have mattered had not overloading been introduced into C++. That changed the rules out of all recognition. The type system was invoked to support overload resolution and suddenly types started to sprout in all directions. For example we now have not one but eight flavours of int (int, const int, volatile int, const volatile int, int&, const int&, volatile int&, const volatile int&). Are all the flavours actually different? Well yes, and no. It all seems to depend on the context. For example when an int& goes out of scope, the underlying memory almost always remains.

Almost always - why not always? Well it might be a reference to a temporary. Now that leads to the interesting question for language lawyers "What is the difference between a local variable and a local reference bound to a temporary?" You don't know? Well don't ask me, because I haven't the vaguest notion.

(When the language lawyers have finished with that question, perhaps they will turn their minds to what sort of type a mutable int is? I guess you cannot have a mutable const int, but can you have a mutable volatile int? These are really tough questions, and need not concern most of us but they highlight the problems that are caused by even apparently sensible minor extensions to the language.)

Whereas const and volatile are cv-qualifiers and, hence, modify the type, mutable is `only' a storage-class-specifier and does not affect the type. Members of a const object are not normally modifiable (e.g., inside a const member function) without casting away const - mutable was provided to obviate the need for the cast in certain well-defined circumstances - Ed.

So far all we have in C++ is C things turned into types. Actually we can do quite a bit with this, particularly when we add in class concepts and conversions, both via constructors and via operators. The fun starts when we add in the next layer: derivation.

At its simplest, derivation just allows us to reuse code even when we do not have access to the original. If it stopped there we wouldn't have much of a problem, but we also wouldn't have the tools for object oriented programming.

This form of derivation often has a sense of refinement or improvement. Its like taking the basic concept of a screw and adding the idea of a cross-head to it. It often suffers from the same problems, something simple and utilitarian becomes more specialised and complicated to use. We can - at a stretch - use a knife on an old plain screw. Knives do not work on machine screws - worse, we need just the right sort of cross head screwdriver if we are not to damage our high-tech screw.

Hidden inside the concept of derivation for reuse is the concept that a derived type is a replacement for the original. To understand what is happening we have to step back and see that the concept of derivation gives us another way to build new types.

The old method is called aggregation or layering. We assemble a new type by wrapping up a number of earlier types into a single package. Aggregation is a little like using Legoreg.: you start with a number of building blocks, push them all together and finish up with something useful though more complicated. It is like building a computer from components, motherboard, power supply etc.

The new method allows us to start with an object and then add modifications to it. That gives us a decision to make. Should we cram a whole lot of bits together (aggregation) or should we modify an existing type (derivation).

Upgrading your computer

Even at this level we can have problems. Is replacing the video card in our computer derivation or aggregation or neither? Think very carefully because I do not think that even this simple real world action can be properly modelled with simple C++ technology.

When we designed our computer class we allowed for replacement video cards because we provided a `pointer to video card' onto which we could attach a specific instance of a subtype of video card. But how do we provide for the enhanced functionality that our new card provides?

Perhaps we should have provided a function pointer for our video driver. Yes, that is obviously the answer. Have fun. Going fully object-oriented can take an awful lot of time.

Note that you cannot derive your SVGA computer from your old VGA one even though the former is a VGA machine because you are replacing a data item and over-riding functionality.

Inheritance versus aggregation

The mythology of object-orientation gives us a simple rule of thumb to decide which approach to use. We are supposed to ask if `X is a Y' or does `X have a Y'?

I deliberately used the term mythology because this question is simplistic and misleading. It doesn't work. It is a lousy criterion and conceals some really serious problems with object-orientation. Problems so serious that I can find no answers within the C++ type system. Let me give you two examples:

A circle and an ellipse

I can remember Francis drumming into us in maths lessons that `a circle is an ellipse'. Mathematically a circle has all the properties of an ellipse. Mathematically a circle is a specialised ellipse.

Now let us look at an ellipse from a C++ type point of view. We sit and list all the functions that we want to apply to an ellipse. One of these functions will involve change of eccentricity either explicitly or through some other change such as magnification in only one dimension.

Actually change of eccentricity is a rather good function to consider - what happens if the eccentricity goes negative? Exactly! The ellipse stops being an ellipse. You see, our names for various conics deal with specific constraints that we can apply to one or more defining properties.

The OO concept of `is a' requires that the derived object can substitute for the original in all cases. In the case of circles and ellipses this is not true. In fact, I know of no way of representing the relationship between a circle and an ellipse in terms of the C++ type system.

Some will claim that I have simply taken a pathological case and that most things fit the type system quite happily. I think that this will be seen to be just about as true as the Victorian attitude to what we now call fractal curves. The unnatural case is not the fractal one but those shapes that have an integral dimension.

Complex numbers and reals

Scott Meyers gives this as another example of `is a' breaking down. He isn't strictly correct because mathematically a real isn't a complex number (with a zero imaginary part). However there is an isomorphism (one to one mapping of both data and operations) between reals and the subset of complex numbers with a zero imaginary part.

It is virtually impossible to represent this relationship in an object-oriented fashion. It makes no difference whether you try to derive complex from real, real from complex or provide a conversion operator; the relationship simply does not fit. The best we can do is to consider whether it is worth providing semi-intelligent division (and perhaps multiplication) to cope with cases where complex operands degenerate to reals.

It is worth noting that most implementations of complex numbers you find in books and magazines completely ignore efficiency in this area. Division of a complex by a real, multiplication of a real by a complex and of a complex by a real should be provided directly and not by converting a real to a complex.

I have written about reals here because I am looking at this from a mathematical view but it is worth noting that there are no reals in computing, only rationals.

Polymorphism

Where a number of sub-types share functionality but differ in implementation of that functionality it makes sense to design an abstract base class that declares the functionality with (pure) virtual functions which will be defined in the derived classes. But if this is what you are doing you should think very carefully before adding functionality in a derived class. If you do so, it will only be available directly through that sub-type. This seems to be an error to my way of thinking.

A cluster of polymorphic types should be interchangeable, whatever one can do the others should be able to do as well, though by a different mechanism.

Perhaps that last paragraph overstates the issue, but I wrote it because so many texts seriously understate it.

Take the example of your Shape hierarchy. The purpose in providing such a hierarchy is precisely because you will not know at compile time what specific shapes will be used. You can only use generic shapes in your program so providing any special feature for a specific shape will be a complete waste - you will not be able to use it.

Inheritance for modification

Though based on substantially the same language mechanisms this use of inheritance is completely different. We are not trying to model a cluster of functionally related objects with different implementations. What we are trying to do is to reuse an earlier implementation by changing or enhancing it.

In this situation I can accept suppression of functionality in the derived class, addition of functionality and even quite radical modification. Some will argue that private bases should be used in such cases. I do not agree. I see nothing wrong with taking table and deriving a folding table from it. You can even use your folding table as a table. However if you want the property of folding you will need to use it as it really is. We are not using polymorphism, we must know that we have a folding table before we can use it as such.

I think that the main motive for RTTI (run time type information) is to try to cater for this double view of inheritance so you can have polymorphism and modification at the same time. Next time you will see that I think such duality is best implemented via multiple inheritance.

Template classes

These add an entire new dimension to the possibilities. They deal with the cases of things that are usually functionally identical, down to implementation detail, but based on unrelated types. Inheritance deals with multiple refinements and specialisations from a single base class. Template classes deal with similarities for distinct, unrelated types.

For example, for type safety a container class needs to be coded for the type of object that it is containing. We need separate linked lists of ints, floats, Shapes etc. We need these because we will often need type specific declarations for variables, parameters and return types even though the functionality is identical.

Polymorphism deals with "same data sets, different implementation details" while template classes deal with "same implementation details, different data sets".

I oversimplify because sometimes a template class will need a specialisation to provide an implementation tuned to a specific data set. But it is the principle that concerns me here.

Summary, different types

* Built-in types, sometimes called scalars.

* Qualified and derived built-in types (pointers, const, reference etc.)

These two groups are related both within each group and between groups by built-in conversions. Any attempt to summarise the rules is about as complicated as simply listing the conversions.

Simple user defined types: enums, unions, structs and classes. The relationships between these are governed by built-in rules (e.g., those for enums) and by user provided specifications (single parameter constructors and conversion operators).

Derived user defined types. For cv-qualification, etc, user defined types follow the same rules that apply to built-ins. Those derived from bases have both a language-provided relationship between base and derived as well as a conceptual relationship. The conceptual relationship can include polymorphism.

Template types (classes) raise another question. What part, if any, do they play in the type system? Before you dismiss this as a trivial question answered with `none', stop and consider the impact of partial specialisation. For example:

template<class T1, class T2> sometype {...};
template<class T> sometype<int, T> {...};
template<class T> sometype<T, int> {...};
sometype<int, int> s;

What happens to this declaration of s?

It should be ambiguous but maybe we'd better wait to see the exact wording in the working paper, since such partial specialisations were only added in Austin - Ed.

Even before we consider multiple inheritance (next issue) we have a rich range of choice. Mixing single inheritance with template classes is really fun.

The problem is that we need to have a very clear idea about the strengths and weaknesses for each method for developing new user defined types. The classic `is a' and `has a' relationships are completely inadequate. As we have seen they do not relate to much of our formal experience in mathematics. The excuse that attempting to derive a square from a rectangle shows failure to analyse the problem domain correctly is a cop out. Show me how to do it properly!

What is the relationship between single and double precision maths? (not just floats and doubles, but complex floats and complex doubles, quaternion varieties, polynomial ones etc.) This would seem to be the domain of template classes even though there will probably be only two (perhaps three with long double) types of each. How do I provide conversions between types based on the same template? To be honest, I do not know. For many the answers are of no importance but for those working in computationally intense areas it matters a lot.

Theoretically, by using member template conversion operators...if anyone can ever get them to work properly - Ed.

Conclusion

I started out to write about multiple inheritance (mixins and addins). Francis persuaded me to think again on the grounds that there was much more to the story. On reflection, I have to agree that he was right though the problem is that much of the rest is like the old maps annotated with `Terra Incognita'.

Before we even begin to think about MI, we need a much better understanding of how to use the C++ type system to develop objects that map the relationships found in the real world.

The Harpist

Related addendum

by Francis Glassborow

I have given a lot of thought to the problem exemplified by the relationship between circles and ellipses. One of the most unfortunate features is that polymorphism is so often explained in terms of Shape and draw(). To get that inheritance graph right requires a deeper understanding of problem domains and OO than is possessed by most.

What is needed is a mechanism for providing polymorphic objects rather than polymorphic types. In other words we need an object that is sometimes a circle exhibiting circle functionality and is sometimes an ellipse with elliptical behaviour. The same object, but two behaviours. I think I can do it but before I write it up for the next issue, I'd be interested to hear your ideas on the subject.

Exercise

Write up a C++ implementation of the relationship between circles and ellipses. Send it in and I'll collate the results and then provide my own answer.

That will be easy because it takes a mind that thinks round corners to tackle the problem and most (if not all) of you will leave it to someone else.

Francis Glassborow

I hope that quite a few of you will prove Francis wrong :-) - Ed.

Multiple inheritance in C++ - part I

by Ulrich W. Eisenecker

This is the first in a series of articles. This part is about the basics of multiple inheritance such as syntax and multiple base classes and their initialisation. As an introduction, I will summarise details of inheritance and virtual functions.

A review of inheritance and late binding

Inheritance is mainly a technique for reusing a description of an existing class to describe a new class. If Derived inherits from Base it means, that in some respect Derived is like Base. Normally one would add further data members or methods to Derived. It is even possible to substitute an inherited method with a new implementation. This may be a complete substitution or an extension, in the sense that there is new code which calls the old implementation. From this point of view it is adequate to speak of a class hierarchy. To illustrate this relation it may be helpful to think of Derived having a Base subobject (Fig. 1). This relationship is not to be confused with a has-part relationship.

Fig. 1: Inheritance between classes

Another important aspect is that, by default, method calls are resolved at compile time (statically). Consider a method m in Base, which is over-ridden in Derived. If a pointer to Derived is assigned to a pointer to Base, invoking m for that pointer will execute Base::m(). Actually, in most circumstances the execution of Derived::m() is wanted. To achieve this, so called late binding is needed, which takes place at run-time. To specify late binding for a method, its declaration in a class is qualified by the keyword virtual. This needs to be done only once (in Base) to be effective for all descendants of Base, but it is not an error to repeat it when declaring a method over-riding m. In the example below, screen output is marked by a preceding ">".

class Base
{
public:
virtual void hello()
{ cout << "Base::hello()\n"; }
};

class Derived : public Base
{
public: // Next use of "virtual" is not
// necessary!
virtual void hello()
{ cout << "Derived::hello()\n"; }
};
...
Base* p = new Base;
p->hello();
p = new Derived;
p->hello();
...
>Base::hello()
>Derived::hello()
In C++, inheritance can be controlled by access specifiers, namely public, protected and private. With public derivation an instance of Derived can always be used when an instance of Base is expected. From this point of view one may speak of a type hierarchy. If inheritance is protected or private, the described assignment and execution of inherited methods no longer works.

class Base
{
public:
virtual void hello() {}
};

class public_Derived : public Base
{
public:
virtual void hello() {}
};

class protected_Derived : protected Base
{
public:
virtual void hello() {}
};

class private_Derived : private Base
{
public:
virtual void hello() {}
};
...
Base* p;
p = new public_Derived; // ok
p = new protected_Derived; // error
p = new private_Derived; // error
This simply means that in C++, a class hierarchy does not necessarily coincide with a type hierarchy. And, in contrast to many other object-oriented programming languages, C++ provides language constructs to explicitly express differences between those hierarchies and therefore to control them.

The need for multiple inheritance

Multiple inheritance is a simple extension of single inheritance in so far as a class can inherit directly from more than one class. Multiple inheritance is often said to be unnecessary. This is not true for at least two reasons:

1. There are cases when modelling using multiple inheritance preserves more of the problem-specific semantics.

2. Due to the inheritance-based polymorphism in C++, multiple inheritance is essential for accessing combined objects by pointers.

Let us look at an example, which is taken from [EIS93], where a phone and a TV form a new device. We start with the following classes:

class Phone
{
public:
virtual void dial(char* number)
{
cout << "Dialling " << number
<< "...\n";
}
};

class TV
{
public:
virtual void switchOn()
{ cout << "TV switched on.\n"; }
};
A first approach to building a two-in-one device could be to make either a TV or a Phone part of a new device called PhoneTV. In either instance you must forward specific requests to the embedded device:

class PhoneTV : public TV
{
Phone aPhone;
public:
virtual void dial(char* number)
{ aPhone.dial(number); }
};

Fig. 2: PhoneTV with single inheritance

An instance of a PhoneTV can be switched on and can be used for dialling a number:

PhoneTV aPhoneTV;
aPhoneTV.switchOn();
aPhoneTV.dial("073127174");
But what happens if a pointer to a TV is initialised with a dynamically created object of type PhoneTV?

TV* aPhoneTV = new PhoneTV;
aPhoneTV->switchOn();
aPhoneTV->dial("073127174");
At least BC 4.0 issues the error "'dial' is not a member of 'TV'". That is because there is no method dial defined for TV, and the information about the availability of dial is lost when assigning a pointer to PhoneTV to a pointer to TV. If, instead, we try:

Phone* aPhoneTV = new PhoneTV;
the compiler complains that it "Cannot convert 'PhoneTV *' to 'Phone *'". The reason is that PhoneTV is not a descendant of Phone.

Without explicit type conversion, pointers to a more specialised class may only be assigned in C++ to a pointer to a public ancestor of this class (i.e., all inheritance provided by public derivation).

This means that polymorphism in C++ works only along the inheritance graph. This can be different in other object-oriented languages. For instance, polymorphism in Smalltalk is signature-based. A Smalltalk-object receiving a message checks whether the signature (message name plus parameters) of the message is known to the object's class or to any of its ancestors. If so, the first method found is executed. Using this technique, called forwarding, (fig. 2) is a common procedure for combining the behaviour of two classes in Smalltalk. Signature-based polymorphism means that there is no need for multiple inheritance in Smalltalk, even though combining classes in this way can be conceptually dirty.

The way to solve the problem with phones and TVs in C++ with only single inheritance is to introduce a common superclass for Phone and TV, which has abstract methods dial and switchOn. But this is not a good design, since the devices which will be combined in future are unknown. That implies the need to change the definition of this superclass whenever another method is needed. This implies many problems: the source code must be available, recompilation is necessary, the semantics of derived classes may be affected, name conflicts may occur if a derived class already has a method with the same name, and so on. Classes, and especially abstract classes, should always be designed to be stable and only be altered as a last resort. The problem of overloaded root classes is well known in languages without multiple inheritance but providing polymorphism through inheritance. See the early versions of C++ (e.g., in The Annotated Reference Manual).

Syntax of multiple inheritance

So all that is necessary is multiple inheritance, and the syntax is quite simple. The classes from which the derived class inherits are listed, separated by commas:

class PhoneTV : public Phone, public TV
{};

Fig. 3: PhoneTV with multiple inheritance

Now all works as expected:

PhoneTV* aPhoneTV = new PhoneTV;
Phone* aPhone;
TV* aTV;
aPhoneTV->switchOn();
aPhoneTV->dial("0731-27174");
aPhone = aPhoneTV;
aPhone->dial("0731-27174");
aTV = aPhoneTV;
aTV->switchOn();
Of course it is possible to mix public, protected and private derivation deliberately.

Initialisation of base classes

As always in C++, there is something going on behind the scenes! Let us add default constructors and destructors to TV and Phone:

class Phone
{
public:
Phone()
{ cout << "Phone\n"; }
virtual ~Phone()
{ cout << "~Phone\n"; }
virtual void dial(char* number)
{
cout << "Dialling " << number
<< "...\n";
}
};
class TV
{
public:
TV()
{ cout << "TV\n"; }
virtual ~TV()
{ cout << "~TV\n"; }
virtual void switchOn()
{ cout << "TV switched on.\n"; }
};
Now it can be shown that the order in which the base classes are declared determines the order in which constructors and destructors of the base classes are called. In the next examples screen output is again marked by a preceding ">":

class PhoneTV : public Phone, public TV
{ };
...
PhoneTV();
...
>Phone
>TV
>~TV
>~Phone

class PhoneTV : public TV, public Phone
{ };
...
PhoneTV();
...
>TV
>Phone
>~Phone
>~TV
This ordering can not be overridden by explicitly calling the constructors of base classes in a different order:

class PhoneTV: public Phone, public TV
{
public:
PhoneTV() : TV(), Phone()
{}
};
...
PhoneTV();
...
>Phone
>TV
>~TV
>~Phone

Disambiguation of name conflicts

What if both Phone and TV have a method mute introduced? For Phone, mute means that transmission of speech is interrupted, until mute is pressed again. When mute is sent to an instance of TV, the speaker volume is set to zero. Pressing mute again, restores volume to its original value. For the purpose of demonstration, the methods just print out their names. All works fine until the moment mute is called. Then it is necessary to resolve the conflict. This is done by qualifying mute with the name of the desired class followed by two colons:

class Phone
{
public:
virtual void dial(char* number)
{
cout << "Dialling " << number
<< "...\n";
}
virtual void mute()
{ cout << "Phone::mute\n"; }
};

class TV
{
public:
virtual void switchOn()
{ cout << "TV switched on.\n"; }
virtual void mute()
{ cout << "TV::mute\n"; }
};

class PhoneTV : public Phone, public TV
{};
...
PhoneTV().Phone::mute();
PhoneTV().TV::mute();
...
A nice challenge is modelling a twin-phone. What about simply deriving it twice from a phone?

class TwinPhone : public Phone, public Phone
{};

Fig. 4: An impossible TwinPhone

As this new class now incorporates two phones, it also has two methods dial. The mechanism introduced above to resolve ambiguities will not work here, because there is no way to distinguish one phone from the other. This is the reason that C++ forbids direct derivation from the same class more than once. However, a class may indirectly inherit a base class any number of times. Conflicting names can then always be disambiguated by providing suitable class scope qualifiers using the :: notation.

Next issue

In the next article, I will introduce virtual base classes using an example from mathematics - combinations, and a program to generate them.

References

[EIS93] Eisenecker, Objektorientierung und Wiederverwendbarkiet. In: unix/mail 6/93, pp420-429.

Ulrich W. Eisenecker

On not mixing it...

by Roger Lever

The articles in Overload 6 by Francis Glassborow (Friends - who needs them?) and Graham Kendall (Putting Jack in the Box) were very interesting, but I wasn't entirely comfortable with the concepts being put forward. So I decided that I would put pen to paper.

Before I put forward a rationale for an alternative approach allow me to establish my credentials - I have none! I work as an Analyst Programmer using mainly Visual Basic, MS Access and Plexus (a 4GL specialising in imaging). My personal interest is in C++ and my experience to date is at the `toys' level, but I take my toys very seriously!

The section entitled "Mixins and printable" (pp10-11) takes an approach with which I am not entirely comfortable. I can see the rationale and it offers a certain elegance but public inheritance should be used to mirror the problem domain and express one of the two (now) classic relationships of:

1. is-a e.g., a car is-a type of, or kind of vehicle

2. has-a e.g., a car has-a engine (also known as composition)

However, the article uses mixin classes (Printable and Storable) and creates an inheritance hierarchy for Record that does not express this is-a or has-a relationship. The article points out that the alternative approach of using has-a fails because:

* You can't instantiate an ABC (i.e., Printable or Storable)

* Late (or dynamic) binding requires an inheritance hierarchy

I shall come back to this thread later, for now I want to move onto a later article within Overload 6.

The section entitled "An answer from the Harpist" (pp22-25) stresses the difference between Object Based Programming (OBP) and Object Oriented Programming (OOP). However, the solution to the problem "Putting Jack in the Box" seems overly complex, in particular the use of contents and container as part of the inheritance hierarchy. The inheritance hierarchy again does not map onto the is-a relationship but is used as a mechanism to enable a polymorphic solution. I'm in favour of an alternative design:

1. The container view of the problem should be expressed with templates

2. The inheritance hierarchies should only use is-a / has-a - like Person

If a solution can be expressed simply then it should be, so opportunities to simplify multiple inheritance should be examined. An example of simplifying a multiple inheritance hierarchy is in section 12.2.2 of Bjarne Stroustrup's C++ Programming Language 2e (pp404-407). More generally, Tom Cargill's C++ Programming Style also offers excellent general advice with a chapter dedicated to unnecessary inheritance.

Both of the articles use inheritance incorrectly when using the strict is-a or has-a interpretation. The mixin approach appears to offer a simple solution to providing printer and disk services and the alternative of using composition fails on the two items quoted above. At least that was the author's contention - I'm not so convinced, but then again I do fall into the category of inexperienced! Everything has a cost, so what are some of the costs with the mixin style?

* The complexity of the software rises (OK! Very subjective :-)

* is-a and has-a inheritance are subverted to use / add a mixin style

* Multiple inheritance is invoked very quickly and also subverted

* Virtual base classes become almost a necessity

* The potential impact of ambiguity (collision of names) rises

* Recompilation costs are increased by the inheritance lattice

My objective is to show an alternate to the mixin design, which uses `proper' inheritance. In the process I also hope to provide an answer to the two quoted objections to using composition.

The key to design is to find the right abstractions for the problem. The two abstractions here are record and device where device could be the screen, printer or hard disk. The important point is that the services required, "printable" and "storable", have been abstracted into a Device. Device can therefore be an ABC, or the base class of an inheritance hierarchy if we want the benefits of dynamic binding. This approach does not subvert is-a as a Printer (or Disk) is-a Device.

Lattice 1                 Lattice 2
Screen Printer Disk Printer Disk
|_______|______| |_______|
| |
Device(ABC) Device(Screen)
If Device is an ABC (Lattice 1 above) then it cannot be instantiated (and the compiler gives an error). However, this would be the preferred approach as it defines the interface for all objects derived from it. However, I started with the second version! (Lattice 2 above) The reason is that I started with just a Device, printing to the screen, and Record. I ran across a number of problems before arriving at this solution. Code implementing this lattice is shown at the end of this article. There is plenty of scope to improve this code, such as using an ABC to define a minimal but complete interface for Device, adding exception handling etc.

The code given below uses the C++ version of multiple polymorphism and also uses a buffer to reduce the coupling between Record and Device. If readers are interested in how exactly I arrived at this point I could be persuaded to bore you some more!

Roger Lever

It would probably be quite educational to see the earlier, discarded, designs that led you to this one - Ed.

// Compiled using Borland 4, Output to a DOS Standard EXE file
// No special code used or Borland specific libraries. Organised into
// two files record.h and main.cpp. All classes were defined inline in a
// single module - this is only suitable as an example.
// Complete listing of the working code which can be cut and pasted into
// a project for experimentation. Starts from here...
// record.h-------------------------------------------------------------
#include <strstrea.h> // provide the buffer service for output
#include <fstream.h> // provide the ofstream extensions to device

// Device default output is to the screen member functions are
// virtual as inheritance will be used to extend this class to
// different types of devices, such as disk, printer, optical...
class Device {
public:
Device(void) { cout << "Device born\n"; }
virtual ~Device(void) { cout << "Device dies\n"; }
virtual void output(ostrstream& os) const {
cout << os.str();
}
};

// Very basic record class inspired by Overload 6. It is declared
// after the Device class since output() takes a Device parameter
// Functions are declared virtual since a derived class will want
// to exploit the polymorphic behaviour especially buildOutput()
class Record {
public:
Record(void) { cout << "Record born\n";}
virtual ~Record(void) {
cout << "Record dies\n"; strm.rdbuf()->freeze(0);
}
virtual void output(Device* dev) { dev->output(strm); }
virtual void buildOutput(void) {
strm << "Build Record output\n" << '\0';
}
protected:
// Protected to enable derived classes to access strm but not
// provide public access to it
ostrstream strm;
};

// Very basic extension of the Record class to demonstrate
// dynamic binding within a derived class which uses the
// inherited interface item output()
class ExtendRecord : public Record {
public:
ExtendRecord(void) { cout << "ExtendRecord born\n"; }
virtual ~ExtendRecord(void) {
cout << "ExtendRecord dies\n";
}
// Override what the derived class wishes to send to output
// but there is no need to override the behaviour of output
virtual void buildOutput(void) {
strm << "Build ExtendRecord output\n" << '\0';
}
};

// Extend device to support generic harddisk services. This class
// should ideally support more options especially filename and
// file access mode
class Disk : public Device {
public:
Disk(void) {
cout << "Disk device created with default hardcoded name\n";
}
virtual ~Disk(void) { cout << "Disk dies\n"; }
virtual void output(ostrstream& os) const {
cout << "Disk writes to rubbish.txt\n";
ofstream out("rubbish.txt", ios::app);
out << "Disk output to a file:-" << os.str();
}
};

// Extend Device to support generic printer services. This class
// would need to encapsulate the horrible details of dealing with
// hardware. For example the `print-stream' may be fine but the
// desired result may not be achieved because the printer is
// disconnected, out of paper...
class Printer : public Device {
public:
Printer(void) { cout << "Printer born\n"; }
virtual ~Printer(void) { cout << "Printer dies\n"; }
virtual void output(ostrstream& os) const {
cout << "Printer output echo to screen\n";
ofstream cprn(4, os.str(), os.pcount());
cprn << os.str();
}
};

// main.cpp--------------------------------------------------------------
#include "record.h"
int main() {
// Uncomment one of the following three devices to show dynamic
// binding in action. Use device for a generic device which prints to the
// screen. This device is extended to include disk and printer services
// Device* pdev = new Device;
Device* pdev = new Disk;
// Device* pdev = new Printer;

// Create an arbitrary record and output to the required device
Record a;
a.buildOutput();
a.output(pdev);
// Create an extended version of record and output to the required device
ExtendRecord b;
b.buildOutput();
b.output(pdev);
// Clean up the new'd item, destructors will cleanup the record objects
delete pdev;
return 0;
}
// End -------------------------------------------------------------------

Mirrored from http://www.accu.org/