Inheritance — Proper Inheritance and Substitutability
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.)
Converting Derived*
→ Base*
works okay; why doesn’t Derived**
→ Base**
work?
Because converting Derived**
→ Base**
would be invalid and dangerous.
C++ allows the conversion Derived*
→ Base*
, since a Derived
object is a kind of a Base
object. However trying
to convert Derived**
→ 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 Car**
→ Vehicle**
, and if you could similarly convert
NuclearSubmarine**
→ Vehicle**
, you could assign those two pointers and end up making a Car*
point at a
NuclearSubmarine
:
class Vehicle {
public:
virtual ~Vehicle() { }
virtual void startEngine() = 0;
};
class Car : public Vehicle {
public:
virtual void startEngine();
virtual void openGasCap();
};
class NuclearSubmarine : public Vehicle {
public:
virtual void startEngine();
virtual void fireNuclearMissile();
};
int 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 !
carPtr->openGasCap(); // This might call fireNuclearMissile()!
// ...
}
In other words, if it were legal to convert Derived**
→ 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
!!). Try the above code out and see what it does —
on most compilers it will call NuclearSubmarine::fireNuclearMissile()
!
(BTW you’ll need to use a pointer cast to get it to compile. Suggestion: try to compile it without a pointer cast to see what the compiler does. If you’re really quiet when the error message appears on the screen, you should be able to hear the muffled voice of your compiler pleading with you, “Please don’t use a pointer cast! Pointer casts prevent me from telling you about errors in your code, but they don’t make your errors go away! Pointer casts are evil!” At least that’s what my compiler says.)
(Note: this FAQ has to do with public
inheritance; private
and protected
inheritance are
different.)
(Note: there is a conceptual similarity between this and the prohibition against converting Foo**
to
const Foo**
.)
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
, which would allow someone to add any
kind of Vehicle
to a parking-lot-of-Car
(assuming parking-lot-of-Vehicle
has a member function like
add(Vehicle&)
). In other words, you could park a Bicycle
, SpaceShuttle
, or even a NuclearSubmarine
in a
parking-lot-of-Car
. Certainly it would be surprising if someone accessed what they thought was a Car
from the
parking-lot-of-Car
, only to find that it is actually a NuclearSubmarine
. Gee, I wonder what the openGasCap()
method would do??
Perhaps this will help: 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 Apple
s!
(Note: this FAQ has to do with public
inheritance; private
and protected
inheritance are
different.)
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
}
int 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 class (e.g., std::array<Derived, 10>
from the standard library) 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.)
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 std::array<T, N>
from the standard library rather than fixed size raw arrays.
(Note: this FAQ has to do with public
inheritance; private
and protected
inheritance are different.)
Is a Circle
a kind-of an Ellipse
?
Depends. But not if Ellipse
guarantees it can change its size asymmetrically.
For example, if Ellipse
has a setSize(x,y)
member function that promises the object’s width()
will be x
and its height()
will be y
, 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 valid relationships between Circle
and Ellipse
:
- Make
Circle
andEllipse
completely unrelated classes - Derive
Circle
andEllipse
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…)
Are there other options to the “Circle
is/isnot kind-of Ellipse
” dilemma?
If you claim that all Ellipse
s 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 revoke one of your claims. You can
get rid of Ellipse::setSize(x,y)
, get rid of the inheritance relationship between Circle
and Ellipse
, or admit
that your Circle
s aren’t necessarily circular. You can also get rid of Circle
completely, where circleness is just a
temporary state of an Ellipse
object rather than a permanent quality of the object.
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, e.g., they might 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…)
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. But I’ll tell you what it does mean — you may not like what I’m about to say: it means your intuitive notion of “kind of” is leading you to make bad inheritance decisions. Your tummy is lying to you about what good inheritance really means — stop believing those lies.
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, where “wrong” means “will cause you to make bad inheritance decisions in OO design/programming.”
Here’s how to make good inheritance decisions in OO design/programming: recognize that the derived class objects must
be substitutable for the base class objects. That means objects of the derived class must behave in a manner
consistent with the promises made in the base class’ contract. Once you believe this, and I fully recognize that you
might not yet but you will if you work at it with an open mind, you’ll see that setSize(x,y)
violates this
substitutability.
There are three ways to fix this problem:
- Soften the promises made by
setSize(x,y)
in base classEllipse
, or perhaps remove that method completely, at the risk of breaking existing code that callssetSize(x,y)
. - Strengthen the promises made by
setSize(x,y)
in the derived classCircle
, which really means allowing aCircle
to have a different height than width — an asymmetrical circle; hmmm. - Drop the inheritance relationship, possibly getting rid of class
Circle
completely (in which case circleness would simply be a temporary state of anEllipse
rather than a permanent constraint on the object).
Sorry, but there simply are no other choices.
You must make the base class weaker (weaken Ellipse
to the point that it no longer guarantees you can set its width
and height to different values), make the derived class stronger (empower a Circle
with the ability to be both
symmetric and, ahem, asymmetric), or admit that a Circle
is not substitutable for Ellipse
.
Important: there really are no other choices than the above three. In particular:
- PLEASE don’t write me and tell me that a fourth option is to derive both
Circle
andEllipse
from a third common base class. That’s not a fourth solution. That’s just a repackaging of solution #3: it works precisely because it removes the inheritance relationship betweenCircle
andEllipse
. - PLEASE don’t write me and tell me that a fourth option is to prevent users from changing the dimensions of an
“Ellipse.” That is not a fourth solution. That’s just a repackaging of solution #1: it works precisely because it
removes that guarantee that
setSize(x,y)
actually sets the width and height. - PLEASE don’t write me and tell me that you’ve decided one of these three is “the best” solution. Doing that would show you had missed the whole point of this FAQ, specifically that bad inheritance is subtle but fortunately you have three (not one; not two; but three) possible ways to dig yourself out. So when you run into bad inheritance, please try all three of these techniques and select the best, perhaps “least bad,” of the three. Don’t throw out two of these tools ahead of time: try them all.
(Note: this FAQ has to do with public
inheritance; private
and protected
inheritance are
different.)
(Note: some people correctly point out that a constant Circle
is substitutable for a constant Ellipse
. That’s
true, but it’s really not a fourth option: it’s really just a special case of option #1, since it works precisely
because a constant Ellipse
doesn’t have a setSize(x,y)
method.)
Perhaps Ellipse should inherit from Circle then?
If Circle
is the base class and Ellipse
is the derived class, then you run into a whole new set of problems. For
example, suppose Circle
has a radius()
method. Then Ellipse
will also need to have a radius()
method, but that
doesn’t make much sense: what does it even mean for a possibly assymetric ellipse to have a radius?
If you get over that hurdle, such as by having Ellipse::radius()
return the average of the major and minor axes or
whatever, then there is a problem with the relationship between radius()
and area()
. Suppose Circle
has an
area()
method that promises to return pi times the square of whatever radius()
returns. Then either
Ellipse::area()
will not return the true area of the ellipse, or you’ll have to stand on your head to get radius()
to return something that matches the above formula.
Even if you get past that one, such as by having Ellipse::radius()
return the square root of the ellipse’s area
divided by pi, you’ll get stuck by the circumference()
method. Suppose Circle
has a circumference()
method that
promises to return two times pi times whatever is returned by radius()
. Now you’re stuck: there’s no way to make all
those constraints work out for Ellipse
: the Ellipse
class will have to lie about its area, its circumference, or
both.
Bottom line: you can make anything inherit from anything provided the methods in the derived class abide by the promises made in the base class. But you ought not to use inheritance just because you feel like it, or just because you want to get code reuse. You should use inheritance (a) only if the derived class’s methods can abide by all the promises made in the base class, and (b) only if you don’t think you’ll confuse your users, and (c) only if there’s something to be gained by using the inheritance — some real, measurable improvement in time, money or risk.
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.)
How could “it depend”??!? Aren’t terms like “Circle” and “Ellipse” defined mathematically?
It’s irrelevant that those terms are defined mathematically. That irrelevance is why “it depends.”
The first step in any rational discussion is to define terms. In this case, the first step is to define the terms
Circle
and Ellipse
. Believe it or not, most heated disagreements over whether class Circle
should/shouldn’t
inherit from class Ellipse
are caused by incompatible definitions of those terms.
The key insight is to forget mathematics and “the real world,” and instead accept as final the only definitions that are
relevant for answering the question: the classes themselves. Take Ellipse
. You created a class with that name, so the
one and only final arbiter of what you meant by that term is your class. People who try to mix “the real world” into the
discussion get hopelessly confused, and often get into heated (and, sadly, meaningless) arguments.
Since so many people just don’t get it, here’s an example. Suppose your program says class Foo : public Bar { ... }
.
This defines what you mean by the term Foo
: the one, final, unambiguous, precise definition of Foo
is given by
unioning the public parts of Foo
with the public parts of its base class, Bar
. Now suppose you decide to rename
Bar
to Ellipse
and Foo
to Circle
. This means that you (yes you; not “mathematics”; not “history”; not
“precedence” nor Euclid nor Euler nor any other famous mathematician; little old you) have defined the meaning of the
term Circle
within your program. If you defined it in a way that didn’t correspond to people’s intuitive notion of
circles, then you probably should have chosen a better label for your class, but nonetheless your definition is the one,
final, unambiguous, precise definition of the term Circle
in your program. If somebody else outside your program
defines the same term differently, that other definition is irrelevant to questions about your program, even if the
“somebody else” is Euclid. Within your program, you define the terms, and the term Circle
is defined by your class
named Circle
.
Simply put, when we are asking questions about words defined in your program, we must use your definitions of those
terms, not Euclid’s. That is why the ultimate answer to the question is “it depends.” It depends because the answer to
whether the thing your program calls Circle
is properly substitutable for the thing your program calls Ellipse
depends on exactly how your program defines those terms. It’s ridiculous and misleading to use Euclid’s definition
when trying to answer questions about your classes in your program; we must use your definitions.
When someone gets heated about this, I always suggest changing the labels to terms that have no predetermined
connotations, such as Foo
and Bar
. Since those terms do not evoke any mathematical relationships, people naturally
go to the class definition to find out exactly what the programmer had in mind. But as soon as we rename the class from
Foo
to Circle
, some people suddenly think they can control the meaning of the term; they’re wrong and silly. The
definition of the term is still spelled out exclusively by the class itself, not by any outside entity.
Next insight: inheritance means “is substitutable for.” It does not mean “is a” (since that is ill defined) and it does not mean “is a kind of” (also ill defined). Substitutability is well defined: to be substitutable, the derived class is allowed (not required) to add (not remove) public methods, and for each public method inherited from the base class, the derived class is allowed (not required) to weaken preconditions and/or strengthen postconditions (not the other way around). Further the derived class is allowed to have completely different constructors, static methods, and non-public methods.
Back to Ellipse
and Circle
: if you define the term Ellipse
to mean something that can be resized asymmetrically
(e.g., its methods let you change the width and height independently and guarantee that the width and height will
actually change to the specified values), then that is the final, precise definition of the term Ellipse
. If you
define the thing called Circle
as something that cannot be resized asymmetrically, then that is also your prerogative,
and it is the final, precise definition of the term Circle
. If you defined those terms in that way, then obviously
the thing you called Circle
is not substitutable for the thing you called Ellipse
, therefore the inheritance would
be improper. QED.
So the answer is always “it depends.” In particular, it depends on the behaviors of the base and derived classes. It does not depend on the name of the base and derived classes, since those are arbitrary labels. (I’m not advocating sloppy names; I am, however, saying that you must not use your intuitive connotation of a name to assume you know what a class does. A class does what it does, not what you think it ought to do based on its name.)
It bothers (some) people that the thing you called Circle
might not be substitutable for the thing you called
Ellipse
, and to those people I have only two things to say: (a) get over it, and (b) change the labels of the classes
if that makes you feel more comfortable. For example, rename Ellipse
to ThingThatCanBeResizedAssymetrically
and
Circle
to ThingThatCannotBeResizedAssymetrically
.
Unfortunately I honestly believe that people who feel better after renaming the things are missing the point. The point is this: in OO, a thing is defined by how it behaves, not by the label used to name it. Obviously it’s important to choose good names, but even so, the name chosen does not define the thing. The definition of the thing is specified by the public methods, including the contracts (preconditions and postconditions) of those methods. Inheritance is proper or improper based on the classes’ behaviors, not their names.
If SortedList
has exactly the same public interface as List
, is SortedList
a kind-of List
?
Probably not.
The most important insight is that the answer depends on the details of the base class’s contract. It is not enough to know that the public interfaces / method signatures are compatible; one also needs to know if the contracts / behaviors are compatible.
The important part of the previous sentence are the words “contracts / behaviors.” That phrase goes well beyond the
public interface = method signatures = method names and parameter types and const
ness. A method’s contract means its
advertised behavior = advertised requirements and promises = advertised preconditions and postconditions. So if the base
class has a method void insert(const Foo& x)
, the contract of that method includes the signature (meaning the name
insert
and the parameter const Foo&
), but goes well beyond that to include the method’s advertised preconditions and
postconditions.
The other important word is advertised. The intention here is to differentiate between the code inside the method
(assuming the base class’s method has code; i.e., assuming it’s not an unimplemented pure virtual function) and the
promises made outside the method. This is where things get tricky. Suppose List::insert(const Foo& x)
inserts a copy
of x
at the end of this
List
, and the override of that method in SortedList
inserts x
in the proper
sort-order. Even though the override behaves in a way that is incompatible with the base class’s code, the inheritance
might still be proper if the base class makes a “weak” or “adaptable” promise. For example, if the advertised promise
of List::insert(const Foo& x)
is something vague like, “Promises a copy of x
will be inserted somewhere within
this
List
,” then the inheritance is probably okay since the override abides by the advertised behavior even though
it is incompatible with the implemented behavior.
The derived class must do what the base class promises, not what it actually does.
The key is that we’ve separated the advertised behavior (“specification”) from implemented behavior (“implementation”), and we rely on the specification rather than the implementation. This is very important because in a large percentage of the cases the base class’s method is an unimplemented pure virtual — the only thing that can be relied on is the specification — there simply is no implementation on which to rely.
Back to SortedList
and List
: it seems likely that List
has one or more methods that have contracts which
guarantee order, and therefore SortedList
is probably not a kind-of List
. For example, if List
has a method that
lets you reorder things, prepend things, append things, or change the i
th element, and if those methods make the
typical advertised promise, then SortedList
would need to violate that advertised behavior and the inheritance would
be improper. But it all depends on what the base class advertises — on the base class’s contract.