[21] Inheritance proper inheritance and substitutability, C++ FAQ Lite
[21] Inheritance proper inheritance and substitutability
(Part of C++ FAQ Lite, Copyright © 1991-98, Marshall Cline, cline@parashift.com)
FAQs in section [21]:
[21.1] Should I hide member functions that were public in
my base class?
[21.2] Derived* > Base* works OK; why doesn't
Derived** > Base** work?
[21.3] Is a parking-lot-of-Car a kind-of
parking-lot-of-Vehicle?
[21.4] Is an array of Derived a kind-of array of Base?
[21.5] Does array-of-Derived is-not-a-kind-of
array-of-Base mean arrays are bad?
[21.6] Is a Circle a kind-of an Ellipse?
[21.7] Are there other options to the "Circle is/isnot
kind-of Ellipse" dilemma?
[21.8] But I have a Ph.D. in Mathematics, and I'm
sure a Circle is a kind of an Ellipse! Does this mean Marshall Cline
is stupid? Or that C++ is stupid? Or that OO is stupid?
[21.9] But my problem doesn't have anything to do
with circles and ellipses, so what good is that silly example to me?
[21.1] Should I hide member functions that were public in
my base class?
Never, never, never do this. Never. Never!
Attempting to hide (eliminate, revoke, privatize) inherited public member
functions is an all-too-common design error. It usually stems from muddy
thinking.
(Note: this FAQ has to do with public inheritance; private and
protected inheritance are different.)
[ Top | Bottom | Previous section | Next section ]
[21.2] Derived* > Base* works OK; why doesn't
Derived** > Base** work?
C++ allows a Derived* to be converted to a Base*, since a Derived object
is a kind of a Base object. However trying to convert a Derived** to a
Base** is flagged as an error. Although this error may not be obvious, it is
nonetheless a good thing. For example, if you could convert a Car** to a
Vehicle**, and if you could similarly convert a NuclearSubmarine** to a
Vehicle**, you could assign those two pointers and end up making a Car*
point at a NuclearSubmarine:
class Vehicle { /*...*/ };
class Car : public Vehicle { /*...*/ };
class NuclearSubmarine : public Vehicle { /*...*/ };
main()
{
Car car;
Car* carPtr = &car;
Car** carPtrPtr = &carPtr;
Vehicle** vehiclePtrPtr = carPtrPtr; // This is an error in C++
NuclearSubmarine sub;
NuclearSubmarine* subPtr = ⊂
*vehiclePtrPtr = subPtr;
// This last line would have caused carPtr to point to sub !
}
In other words, if it was legal to convert a Derived** to a Base**, the
Base** could be dereferenced (yielding a Base*), and the Base* could be
made to point to an object of a different derived class, which could
cause serious problems for national security (who knows what would happen if
you invoked the openGasCap() member function on what you thought was a Car,
but in reality it was a NuclearSubmarine!!)..
(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)
[ Top | Bottom | Previous section | Next section ]
[21.3] Is a parking-lot-of-Car a kind-of
parking-lot-of-Vehicle?
Nope.
I know it sounds strange, but it's true. You can think of this as a
direct consequence of the previous FAQ, or you can reason it this way: if the
kind-of relationship were valid, then someone could point a
parking-lot-of-Vehicle pointer at a parking-lot-of-Car. But
parking-lot-of-Vehicle has a addNewVehicleToParkingLot(Vehicle&) member
function which can add any Vehicle object to the parking lot. This would
allow you to park a NuclearSubmarine in a parking-lot-of-Car. Certainly it
would be surprising if someone removed what they thought was a Car from the
parking-lot-of-Car, only to find that it is actually a NuclearSubmarine.
Another way to say this truth: a container of Thing is not a
kind-of container of Anything even if a Thing is a kind-of an
Anything. Swallow hard; it's true.
You don't have to like it. But you do have to accept it.
One last example which we use in our OO/C++ training courses: "A
Bag-of-Apple is not a kind-of Bag-of-Fruit." If a
Bag-of-Apple could be passed as a Bag-of-Fruit, someone could
put a Banana into the Bag, even though it is supposed to only contain
Apples!
(Note: this FAQ has to do with public inheritance; private and
protected inheritance are different.)
[ Top | Bottom | Previous section | Next section ]
[21.4] Is an array of Derived a kind-of array of Base?
Nope.
This is a corollary of the previous FAQ. Unfortunately this one can get you
into a lot of hot water. Consider this:
class Base {
public:
virtual void f(); // 1
};
class Derived : public Base {
public:
// ...
private:
int i_; // 2
};
void userCode(Base* arrayOfBase)
{
arrayOfBase[1].f(); // 3
}
main()
{
Derived arrayOfDerived[10]; // 4
userCode(arrayOfDerived); // 5
}
The compiler thinks this is perfectly type-safe. Line 5 converts a Derived*
to a Base*. But in reality it is horrendously evil: since Derived is
larger than Base, the pointer arithmetic done on line 3 is incorrect: the
compiler uses sizeof(Base) when computing the address for
arrayOfBase[1], yet the array is an array of Derived, which means the
address computed on line 3 (and the subsequent invocation of member function
f()) isn't even at the beginning of any object! It's smack in the middle of
a Derived object. Assuming your compiler uses the usual approach to
virtual functions, this will reinterpret the
int i_ of the first Derived as if it pointed to a virtual table, it
will follow that "pointer" (which at this point means we're digging stuff out
of a random memory location), and grab one of the first few words of memory at
that location and interpret them as if they were the address of a C++ member
function, then load that (random memory location) into the instruction pointer
and begin grabbing machine instructions from that memory location. The chances
of this crashing are very high.
The root problem is that C++ can't distinguish between a pointer-to-a-thing and
a pointer-to-an-array-of-things. Naturally C++ "inherited" this feature from
C.
NOTE: If we had used an array-like class (e.g., vector<Derived> from
STL) instead of using a raw array, this problem would have been
properly trapped as an error at compile time rather than a run-time disaster.
(Note: this FAQ has to do with public inheritance; private and
protected inheritance are different.)
[ Top | Bottom | Previous section | Next section ]
[21.5] Does array-of-Derived is-not-a-kind-of
array-of-Base mean arrays are bad?
Yes, arrays are evil. (only half kidding).
Seriously, arrays are very closely related to pointers, and pointers are
notoriously difficult to deal with. But if you have a complete grasp of why
the above few FAQs were a problem from a design perspective (e.g., if you
really know why a container of Thing is not a kind-of container of
Anything), and if you think everyone else who will be maintaining your
code also has a full grasp on these OO design truths, then you should feel free
to use arrays. But if you're like most people, you should use a template
container class such as vector<T> from STL rather than raw
arrays.
(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)
[ Top | Bottom | Previous section | Next section ]
[21.6] Is a Circle a kind-of an Ellipse?
Not if Ellipse promises to be able to change its size asymmetrically.
For example, suppose Ellipse has a setSize(x,y) member function, and
suppose this member function promises the Ellipse's width() will be x,
and its height() will be y. In this case, Circle can't be a kind-of
Ellipse. Simply put, if Ellipse can do something Circle can't, then
Circle can't be a kind of Ellipse.
This leaves two potential (valid) relationships between Circle and Ellipse:
Make Circle and Ellipse completely unrelated
classes
Derive Circle and Ellipse from a base class representing
"Ellipses that can't necessarily perform an unequal-setSize()
operation"
In the first case, Ellipse could be derived from class AsymmetricShape, and
setSize(x,y) could be introduced in AsymmetricShape. However Circle
could be derived from SymmetricShape which has a setSize(size) member
function.
In the second case, class Oval could only have setSize(size) which sets
both the width() and the height() to size. Ellipse and Circle
could both inherit from Oval. Ellipse but not Circle could add the
setSize(x,y) operation (but beware of the hiding rule
if the same member function name setSize() is used for both operations).
(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)
(Note: setSize(x,y) isn't sacred. Depending on your goals, it may be okay to
prevent users from changing the dimensions of an Ellipse, in which case it
would be a valid design choice to not have a setSize(x,y) method in
Ellipse. However this series of FAQs discusses what to do when you want to
create a derived class of a pre-existing base class that has an
"unacceptable" method in it. Of course the ideal situation is to discover this
problem when the base class doesn't yet exist. But life isn't always ideal...)
[ Top | Bottom | Previous section | Next section ]
[21.7] Are there other options to the "Circle is/isnot
kind-of Ellipse" dilemma?
If you claim that all Ellipses can be squashed asymmetrically, and you claim
that Circle is a kind-of Ellipse, and you claim that Circle can't be
squashed asymmetrically, clearly you've got to adjust (revoke, actually) one of
your claims. Thus you've either got to get rid of
Ellipse::setSize(x,y), get rid of the inheritance relationship between
Circle and Ellipse, or admit that your Circles aren't necessarily
circular.
Here are the two most common traps new OO/C++ programmers regularly fall into.
They attempt to use coding hacks to cover up a broken design (they redefine
Circle::setSize(x,y) to throw an exception, call abort(), choose the
average of the two parameters, or to be a no-op). Unfortunately all these
hacks will surprise users, since users are expecting width() == x and
height() == y. The one thing you must not do is surprise your users.
If it is important to you to retain the "Circle is a kind-of Ellipse"
inheritance relationship, you can weaken the promise made by Ellipse's
setSize(x,y). E.g., you could change the promise to, "This member function
might set width() to x and/or it might set height() to
y, or it might do nothing". Unfortunately this dilutes the contract
into dribble, since the user can't rely on any meaningful behavior. The whole
hierarchy therefore begins to be worthless (it's hard to convince someone to
use an object if you have to shrug your shoulders when asked what the object
does for them).
(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)
(Note: setSize(x,y) isn't sacred. Depending on your goals, it may be okay to
prevent users from changing the dimensions of an Ellipse, in which case it
would be a valid design choice to not have a setSize(x,y) method in
Ellipse. However this series of FAQs discusses what to do when you want to
create a derived class of a pre-existing base class that has an
"unacceptable" method in it. Of course the ideal situation is to discover this
problem when the base class doesn't yet exist. But life isn't always ideal...)
[ Top | Bottom | Previous section | Next section ]
[21.8] But I have a Ph.D. in Mathematics, and I'm
sure a Circle is a kind of an Ellipse! Does this mean Marshall Cline
is stupid? Or that C++ is stupid? Or that OO is stupid?
Actually, it doesn't mean any of these things. The sad reality is that it
means your intuition is wrong.
Look, I have received and answered dozens of passionate e-mail messages about
this subject. I have taught it hundreds of times to thousands of software
professionals all over the place. I know it goes against your intuition. But
trust me; your intuition is wrong.
The real problem is your intuitive notion of "kind of" doesn't match the OO
notion of proper inheritance (technically called "subtyping"). The bottom line
is that the derived class objects must be substitutable for the base
class objects. In the case of Circle/Ellipse, the setSize(x,y) member
function violates this substitutability.
You have three choices: [1] remove the setSize(x,y) member function from
Ellipse (thus breaking existing code that calls the setSize(x,y) member
function), [2] allow a Circle to have a different height than width (an
asymmetrical circle; hmmm), or [3] drop the inheritance relationship. Sorry,
but there simply are no other choices. Note that some people mention the
option of deriving both Circle and Ellipse from a third common base
class, but that's just a variant of option [3] above.
Another way to say this is that you have to either make the base class weaker
(in this case braindamage Ellipse to the point that you can't set its width
and height to different values), or make the derived class stronger (in this
case empower a Circle with the ability to be both symmetric and, ahem,
asymmetric). When neither of these is very satisfying (such as in the
Circle/Ellipse case), one normally simply removes the inheritance
relationship. If the inheritance relationship simply has to exist, you
may need to remove the mutator member functions (setHeight(y), setWidth(x),
and setSize(x,y)) from the base class.
(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)
(Note: setSize(x,y) isn't sacred. Depending on your goals, it may be okay to
prevent users from changing the dimensions of an Ellipse, in which case it
would be a valid design choice to not have a setSize(x,y) method in
Ellipse. However this series of FAQs discusses what to do when you want to
create a derived class of a pre-existing base class that has an
"unacceptable" method in it. Of course the ideal situation is to discover this
problem when the base class doesn't yet exist. But life isn't always ideal...)
[ Top | Bottom | Previous section | Next section ]
[21.9] But my problem doesn't have anything to do
with circles and ellipses, so what good is that silly example to me?
Ahhh, there's the rub. You think the Circle/Ellipse example is
just a silly example. But in reality, your problem is an isomorphism
to that example.
I don't care what your inheritance problem is, but all (yes all) bad
inheritances boil down to the Circle-is-not-a-kind-of-Ellipse example.
Here's why: Bad inheritances always have a base class with an extra
capability (often an extra member function or two; sometimes an extra promise
made by one or a combination of member functions) that a derived class can't
satisfy. You've either got to make the base class weaker, make the derived
class stronger, or eliminate the proposed inheritance relationship. I've
seen lots and lots and lots of these bad inheritance proposals, and believe me,
they all boil down to the Circle/Ellipse example.
Therefore, if you truly understand the Circle/Ellipse example, you'll be
able to recognize bad inheritance everywhere. If you don't understand what's
going on with the Circle/Ellipse problem, the chances are high that you'll
make some very serious and very expensive inheritance mistakes.
Sad but true.
(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)
[ Top | Bottom | Previous section | Next section ]
E-mail the author
[ C++ FAQ Lite
| Table of contents
| Subject index
| About the author
| ©
| Download your own copy ]
Revised May 27, 1998
Wyszukiwarka
Podobne podstrony:
PropertiesZjava beans PropertyChangeEventThe Pacific Pt VI PROPER HDTV XviD NoTVBasicScrollPaneUI PropertyChangeHandlernamespacekeycode 1 1 propertiesPropertiesOBasicComboPopup PropertyChangeHandlerPropertiesDPropertiesD2 1 Properties Involving Mass a03 Relationship between electrochemical properties of SOFC cathode andfunction aggregate properties by listjava util Propertiesjava beans PropertyEditorwięcej podobnych podstron