www.forum.vingrad.ru - форумы по программированию. Программирование на С++
Задать вопрос на форуме по C++


[21] Inheritance -- proper inheritance and substitutability, C++ FAQ Lite

[21] Inheritance — proper inheritance and substitutability
(Part of C++ FAQ Lite, Copyright © 1991-2005, Marshall Cline, cline@parashift.com)


FAQs in section [21]:


[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.)

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[21.2] Converting Derived* Base* works OK; 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 fireNuclearMissle();
 };
 
 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 fireNuclearMissle()!
   
...
 }

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::fireNuclearMissle()!

(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.)

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[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, 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 Apples!

(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[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
 }
 
 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-like class (e.g., std::vector<Derived> 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.)

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[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 std::vector<T> from the standard library rather than raw arrays.

(Note: this FAQ has to do with public inheritance; private and protected inheritance are different.)

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[21.6] 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:

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...)

TopBottomPrevious sectionNext section