Jesse Liberty wrote:
In the July/August 1994 C++ Report Scott Meyer's makes the astonishing
"Meyers", please. Otherwise people confuse me with Bertrand Meyer or
the ballet dancer in the XVT print ads. I'm not sure which is worse :-)
recommendation that you *never* derive from a concrete class; that all
derivation should be from abstract base types. If it were anyone else
making such a bizzarre recommendation, I'd probably dismiss it; but I find
Meyer's book Effective C++ to be one of the most useful books I've ever
read.
I see a host of problems with this recommendation, however. Let's take a
simple example: I have the following hierarchy:
sendable <- storable <- readable <- parent
All items which can be sent between computers are sendable (such as a
service request). All items which can be put on disk are storable (such as
a file), and all storables are also sendable. Of those, some are articles
which can be read in a viewer, and of those some have child articles and
thus a list of cross references.
If I take Meyers seriously, I'm forced to say either that I'll have no
objects which are simply readable but not parents, or that both readable
and parent must inherit from a common base class (say absReadable). But if
readable adds nothing except implementation to the abstract base class
absReadable, what have i gained? Also, what if i have a storable object
(say an index) which is not readable? Must I then create a new abstract
base class? And if I want to derive from parent, must i now make parent an
abstract base class and twice derive from *it*??
Multiple inheritance will solve some of this, but at the price of having to
struggle with m.i. And if I do use M.I., do i then have to say that a
class derived from parent must have 4 base classes, just to get this
functionality??
This seems wrong headed in the extreme. I wondered what others thought.
Before going further, it is important to remember that there was a
specific technical motivation for the advice in my column: it is all but
impossible to write a reasonably-behaving operator= for non-leaf
concrete classes (but see below). This kind of problem manifests itself
only in the code itself, not in high-level diagrams such as the above.
So a reasonable question at this point is this: how do you handle
assignments in the classes above? How do you avoid the problems I
identified in my column?
Robert Martin responded to Jesse's posting:
>But if
>readable adds nothing except implementation to the abstract base class
>absReadable, what have i gained?
You have gained this. Clients of your object will use the interface
declared in absReadable. Thus, these clients will not depend upon the
implementation of class 'readable'. You have broken this dependency.
Thus, you may not implement many different kinds of 'readable'
I think Robert meant "may implement," not "may not implement."
classes, classes which use different algorithms to support the
'readable' interface. The clients of absReadable will neither know
nor care.
>Multiple inheritance will solve some of this, but at the price of having to
>struggle with m.i. And if I do use M.I., do i then have to say that a
>class derived from parent must have 4 base classes, just to get this
>functionality??
Yes. First of all, there is little to struggle with. MI adds "one
comma per multiple base class" in terms of source complexity.
Except that this leads to the need for virtual base classes, and more
than once I've gone on the record for advocating abstinance from virtual
bases (if possible). ("Multiple inheritance: just say no.") The
ultimate additional complexity of MI cannot be measured in commas. The
complexity is conceptual and semantic, not syntactic. The method I
described in my column of eliminating the need for concrete classes
inheriting from concrete classes (which is described by Jesse below)
does NOT involve the use of MI.
Thus, I would create four abstract classes, with interfaces that have
nothing but pure virtual functions. These four classes are unrelated,
but can be inherited as multiple bases into my objects.
class MyObject : public Storable,
public Writable,
public Readable,
public Parent
{
// implement the interfaces.
};
In general, the rule that you should not derive from a concrete class
is a good one. There are times when it is impractical, such as when
the base class is part of a third party hierarchy. But where
practical, it is a good rule to follow.
The reason is simple. Clients will depend upon the abstract
interfaces, not upon the implementations. Thus the implementations
are free to change and diversify, without affecting the clients. And
this freedom to change and diversify is what OO is all about.
Jesse followed up as follows:
I think I've decided to subscribe to your postings :-) That was an
incredibly helpful and thoughtful answer, thanks. Now, what about class
libraries? Ought they create abstract base classes for every concrete class
they wish to offer?
No, they should provide abstract classes for every conceptual
abstraction they wish to support. My next column in The C++ Report
(currently scheduled for November) will examine reader comments on my
July/August column, but the column after that (currently slated for
January) will explain in some detail part of my philosophy on when to
introduce abstract classes. In brief, I believe that the desire to have
a new concrete class inherit from an existing concrete class indicates
the absence in the hierarchy of a useful abstraction, so my advice to
introduce a new abstract class to eliminate the need to have concretes
inherit from other concretes has the beneficial side-effect of
identifying useful abstractions and of forcing programers to represent
them explicitly.
Also, does it violate the integrity of what you are putting forth if i have
the following structure:
AbsClass A
ConcreteA : Public AbsClassA;
AbsClassB : Public AbsClass A;
ConcreteB : PUblic AbsClassB;
AbsClassC : Public AbsClass B;
ConreteC : Public AbsClass C;
This is in fact what I advocate in my column. Note the lack of a need
for MI and the existence of three new abstractions, each of which *must*
be useful. How do we know they are useful? Because each has more than
one child, hence each is useful in more than one context.
Joachim Zobel wrote about my claim that it's impossible to write a
correctly-behaving operator= for non-leaf classes;
Among other argument he [Scott] says that inheritance from a concrete
class makes operator = unsafe, even if it is virtual.
I think I can circumvent the problem by inventing something like
2-dimensional virtuality. Its like this:
His revised posting stated:
The problem is: Assume we have three concrete classes, Animal, Lizard and
Chicken, where Lizar and Chicken are derived from Animal. Then with
Animal ann;
Chicken chick;
Lizard liz;
Animal *pliz=&liz, *pann=&ann, *pchick=&chick;
*pliz=*pann; //should be ok, but my previous example didn't accept this
Actually, I believe this should be an error under normal circumstances,
because it doesn't conceptually make sense to assign only an animal to
a lizard -- what data should go into the lizard-specific data members?
If you follow my recommendation that all non-leaf classes be abstract,
this will not compile.
*pliz=*pchick; //Should give an error
class Animal
{
virtual void assignToAnimal(Animal &) const;
protected:
virtual void assignAnimalDown(Animal &) const;
public:
virtual Animal & operator=(const Animal &);
};
class Lizard: public Animal
{
virtual void assignToAnimal(Animal &) const;
virtual void assignAnimalDown(Animal &) const;
virtual void assignToLizard(Lizard &) const;
protected:
virtual void assignLizardDown(Lizard &) const; //Needed for derived class
public:
virtual Lizard & operator=(const Animal &);
};
class Chicken: public Animal
//The same as Lizard
void Animal::assignAnimalDown(Animal &a) const
{
Animal::assignToAnimal(a); //only the derived functions differ
}
void Lizard::assignAnimalDown(Animal &) const
{
//Exception
}
Animal & Animal::operator=(const Animal &a)
{
a.assignToAnimal(*this);
return *this;
}
Lizard & Lizard::operator=(const Animal &a)
{
const Lizard *liz=dynamic_cast<const Lizard *>&a;
if (!liz)
a.assignAnimalDown(*this);
else
liz->assignToLizard(*this);
return *this;
}
I still hope my use of dynamic cast is correct, I've never used it before.
It looks okay to me, but you should bear in mind that you can also
dynamically cast to a reference. If the cast fails, an exception is
thrown.
Lizard::assignToAnimal should also give an exception (if there isn't a
sensible way to do this assignment) while Lizard::assignToLizard
and Animal::assignToAnimal should simpyly do the assignments.
It looks like an operator= that is virtual in both arguments is a pretty
expensive thing.
This looks like an implementation of what is usually called
double-dispatching, although I've never seen it in quite this form
before. Double-dispatching can be made to work for operator= (as well
as for any other function), so I suppose Joachim has successfully shown
that my claim that writing an operator= that behaves correctly is
"impossible" is untrue. It does, however, shift error detection from
compile time to runtime, which I generally consider to be a
disadvantage, and it's rather difficult to comprehend (hence to
maintain). Even Joachim seems to doubt that all the trouble is
compensated for by the resulting flexibility in assignments.
Nonetheless, double-dispatching is a technique worth knowing, because
sometimes you really do want a function that binds virtually on the
type of more than one object.
As fate would have it, I'm going out of town tomorrow for about three
weeks, so I am unlikely to see followups to this posting, if there are
any. If you'd like to make comments and you'd like to make sure I see
them, feel free to send me mail at sme...@netcom.com.