C++ programmers come across inheritance at an early stage in their acquisition of programming knowledge. However I have long had an itch in this area based on the feeling that too often what they are taught as well as what they acquire from reading and following examples is not exactly the way it should be done. In this article I want to explore my thoughts on the subject and I hope that you will all read them critically. In other words, I will be profoundly unhappy if Sean gets no response.
There are three prime reasons (that I know of) for using inheritance:
- To reuse an existing class with modifications.
- To provide an extension, to an existing class by adding functionality usually with extra data.
- To support polymorphic types.
To keep the amount of code under control most of my examples will contain just enough to support the text.
Inheritance for reuse
Though many books avoid making this explicit, most of them provide examples of inheritance that are exactly this. Consider:
class Miss {
public:
// standard constructors etc.
float do_something ();
float do_ok();
};
Assume that this class almost provides a solution to my client's problem but I must have do_something() return a double. Being an ethical programmer I avoid the cop-out of redefining the problem to align it with the available solution. However I do not want to have to rewrite (or even simply clone and modify) the class in its entirety. We all know what happens if you touch existing code. If left to the information provided by most books, inexperienced programmers come up with:
class Poor_Solution: public Miss {
public:
double do_something();
};
I think this is a mistake. Poor_Solution instances may share much behaviour with Miss ones but they break the substitution rule, they behave differently in at least one instance. We must look again at our concepts. Our use of Miss to provide Solution is an implementation shortcut. Implementation details should be private, or at the very least protected. We have two mechanisms available to represent this relationship:
class Solution: Miss { // private inheritance
// interfaces
};
or
class Solution {
Miss s; // layering or aggregation
// interfaces
};
I have difficulty with distinguishing these choices and I would welcome someone writing an article on the advantages and disadvantages of each. The differences seem subtle and I suspect that each method has its uses.
In both cases the programmer now has to provide a complete public interface rather than rely on inheritance. This is a very small cost. Each member function that you wish to import from the Miss interface must be provided by an inline wrapper. For example:
float do_ok() { return s.do_ok(); }
The advantage that you get is that instances of Solution cannot accidentally be passed to functions that want a reference or pointer to a Miss object. If you doubt that this could matter, then remember that such functions rely on Miss behaviour and what you have given them is an object that is intended to have an alternate behaviour. In my example the difference is miniscule but in real code the changed behaviour is likely to be more substantial.
I suggest that the coding rule should be:
If you want a variant of an existing type that has different behaviour then use either private inheritance or layering. Do not use public inheritance.
Just to show how different languages have different philosophies, Smalltalk books advocate inheritance for reuse. But support for inheritance is rather different between the two languages. - Ed.
Inheritance for enhancement
At first sight this is the same as 'Inheritance for Reuse' but a second glance reveals that this is not the case. Consider the relationship between istream and ifstream. An ifstream instance should be able to substitute as an istream one. In other words it should present the whole of the istream interface plus, possibly, some extra elements. Note that I am writing about interfaces, not implementation. Of course some aspects of the istream interface may be implemented differently for an ifstream one.
These potential differences are clearly identified in the base class because they have been declared virtual. The only places that you should over-ride a public base class implementation is where the functions are virtual ones. This ensures that all functions that take references or pointers as parameters will behave correctly when supplied a derived type argument.
If you want to over-ride non-virtual functions in a class that you do not own, you must go through two stages. First use private inheritance to create a new base class with the appropriate virtual qualified functions then derive from that.
Another feature of this situation is that you should consider some form of smart pointer to handle dynamic instances of these classes. If you neglect this and use ordinary pointers you will find that your code is not exception safe. In other words you will be leaking resources when exceptions are thrown over dynamic instances.
If exceptions are your only concern you can make do with a pretty simple smart pointer but if you want to use containers of mixtures of base and derived instances you will need to provide a smarter pointer. If you want to use STL containers and algorithms where exceptions may occur you should know that some combinations of STL containers and algorithms are unsafe in the presence of an exceptions.
Clearly there are good uses for 'inheritance for enhancement', it is one of the pillars underpinning the iostream part of the Standard C++ library, however inexperienced programmers should stick to the principle 'only change what the original (base class) designer anticipated changing'.
I could write much more but this is not intended to be the main focus of this article, not least because it is one of the hardest aspects of inheritance to get right.
Do not over-ride non-virtual member functions of a public base class.
Inheritance for pure polymorphism
Fundamentally the concept of polymorphism is one of providing multiple implementations for a single interface. The implementation will be chosen in the runtime context, so called late or dynamic binding.
When I use the term 'pure polymorphism' I am referring to a type where all instances have identical interfaces even if the implementation (and therefore the detailed behaviour) may be different. Such objects are characterised by a complete set of virtual functions for the polymorphic behaviour coupled with any suitable functions to supply invariant behaviour. Derived classes will only provide constructors, destructor, member data and over-rides (implementations) of virtual base functions. There will be no extra public member functions and public static members. In other words, the public interface of all derived classes must be exactly that of the base class.
You may recognise this as the kind of situation where you provide an Abstract Base Class. True, but that is an implementation technique which should be hidden from the user. Good coding depends on abstraction and encapsulation to hide complexity. Expecting the application programmer to use pointers (smart or otherwise) to manage concrete instances of a pure polymorphic type is leaving your job half done. The user of your polymorphic type should not need to know of the mechanisms. For example, the user of a Shape abstraction should be able to create and use Shape objects based entirely on the public interface of a Shape. Yes, read that again and realise that an ABC cannot meet that expectation because there are no objects of an ABC type. I want to spend the remainder of this article introducing you to a simple method that will provide instances of a polymorphic type so that you can clone them, copy assign them, have containers of them etc.
Though it may not be the most perfect of designs, I am going to use International postal addresses as my example because it is something we all understand and I can provide a very simple set of interfaces.
Let me start by writing the interface I want, and then see if there is some way I can achieve it.
class Address {
// the works
public:
Address (); // default constructor
// for containers
Address (String country,
iostream init_data = cin);
Address (const Address&); // clone a
// specific address
~Address ();
Address & operator = (const Address &); const
Address & printon (ostream & out = cout) const;
Address & readfrom (istream & in = cin);
};
The real crunch issue is copying. Different countries will have different amounts of data. Worse, copy assignment will not be of use unless we can manage copying between addresses of different types. Let us back up a little and create an abstract base class:
class Address_Data {
// suppress cloning
Address_Data (const Address_Data &);
Address_Data & operator = (const
Address_Data &); // and copying
public:
Address_Data (); virtual ~Address_Data (){}
virtual const Address_Data &
printon (ostream & out = cout) const = 0;
virtual Address_Data &
readfrom (istream & in = cin) =0;
};
Note that I have had to remove the copying functionality because you cannot copy an abstraction. I have also removed the second constructor because it does not have any meaning in context. However what I really need is some form of 'virtual constructor' so that I can construct the correct type of address when I know what it is. As I cannot have virtual constructors I had better declare a couple of functions to do the work:
virtual Address_Data*
clone_address_data() = 0;
Address_Data * make_address_data
(const String & country,
iostream & data = cin);
The first of these will be a pretty straightforward. The second will be a hack, anyone with a better solution should write it up and send it in. For the first, each concrete class derived from Address_Data, for example UK_Address_Data will provide:
Address_Data * clone_address_data()
{return new UK_Address_Data (*this);
}
These relies on each concrete class having a copy constructor available.
Next, every concrete class derived from Address_Data, will include a constructor of the form:
UK_Address_Data(iostream in = cin) {
readfrom(in);
}
Then we can implement make_address_data() with:
Address_Data *
make_address_data(const String & country,
iostream & data){
if (!strcmp(country, "UK"))
return new
UK_Address_Data(data);
if (!strcmp(country, "USA"))
return new
USA_Address_Data(data);
// etc., one per possible country
return 0; // default to null pointer
}
Note that this function will be a static member function of Address_Data because it is called to make concrete instances of addresses. It will also need to be changed when new concrete classes are added so the implementation code needs to be separate from any other code so that it can be updated independently.
As you will see shortly, I do not need to pro vide a copy assignment for the concrete classes.
Now let me look at a typical complete concrete class.
class UK_Address_Data:
public Address_Data
{
friend class Address_Data;
// provide
// access for the abstraction
// provide for appropriate data
UK_Address_Data(const UK_Address_Data &);
// to be implemented
UK_Address_Data & operator = (
const UK_Address_Data &);
// suppress copying
UK_Address_Data (iostream & in = cin)
{ readfrom(in); };
~UK_Address_Data (){
//delete data if necessary
)
const Address & printon(ostream & out = cout) const;
Address & readfrom (istream & in = cin);
Address_Data * clone_address_data()
};
Now go back to Address_Data and remove public so that the ABC is entirely private as will be all its derived concrete classes. In other words nothing can get at any part of the implementation. We now have to provide a channel by which exactly one thing can get at this hierarchy. In Address_Data we declare Address a friend. By the way, I believe that the uses of friend in this example are exactly what friendship is all about. It is to support implementation hiding, not to expose data just because it is convenient for lazy or incompetent programmers.
Now let me go back to the original class and see how I can use my ABC rooted hierarchy to solve my problem.
class Address {
Address_Data * the_works;
public:
Address (): the_works(0) {} // default
// constructor for containers
Address (String country,
iostream init_data =cin ) :
the_works(make_address_data(country,
init_data)) {}
Address (const Address &. a) : the_works(a.clone_address_data()){}
~Address ()
{ delete the_works, the_works=0; }
Address & operator=(const Address & a)
{ Address * temp =
a.clone_address_data();
delete the_works, the_works = temp;
return * this ;
)
const Address & printon (ostream & out = cout) const
{ the_works -> printon(out);
return *this;
}
Address & readfrom {
istream & in = cin) {
the_works->readfrom(in);
return *this;
}
};
Note that there is precisely one change to my original. I have provided a single item of data through which I can access my carefully honed extensible implementation. Oh, I have added the implementation of the functions in class because they are simple enough to be in-line. Do you agree?
No doubt many of you can pick at the above code and argue about the design. Good, you should not accept something just because it is in print. However I think that the principle is right. Some of you may have recognised Address as being a surrogate class. If you did not and have wondered what the term means, now you know.
Anytime you find yourself handling dynamic instances of a polymorphic type with pointers I suggest you should step back and consider if this is essential or just a feature of a poor design. Indeed, anytime you find yourself using new and delete at the application level you have cause to investigate. Currently so many libraries are poorly designed so you may have to resort to such complexity but...
I believe that the reason that C++ appears to have such a steep learning curve is that too many tools are half finished. This is compounded by the old hands continuing to use idioms that they have imported from C without realising that there are better alternatives in C++. You can use C++ as if it was C, use overt arrays of pointers etc. but you do not have to.
I have deliberately left one member function of the
Address and Address_Data hierarchy poorly designed and
implemented. There is a copy of 'Ruminations on C++' waiting for
the person who provides the best correction to this fault. Of
course, first you must identify it.
Overload Journal #17/18 - Jan 1997 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 1718
(9)
All > Topics > Programming (877) Any of these categories - All of these categories |