Day 14. Polymorphism

On Day 12, “Implementing Inheritance,” you learned how to write virtual functions in derived classes. This is the fundamental building block of polymorphism: the capability to bind specific, derived class objects to base class pointers at runtime.

Today, you will learn

• What multiple inheritance is and how to use it

• What virtual inheritance is and when to use it

• What abstract classes are and when to use them

• What pure virtual functions are

Problems with Single Inheritance

Suppose you’ve been working with your animal classes for a while, and you’ve divided the class hierarchy into Birds and Mammals. The Bird class includes the member function Fly(). The Mammal class has been divided into a number of types of Mammals, including Horse. The Horse class includes the member functions Whinny() and Gallop().

Suddenly, you realize you need a Pegasus object: a cross between a Horse and a Bird. A Pegasus can Fly(), it can Whinny(), and it can Gallop(). With single inheritance, you’re in quite a jam.

With single inheritance, you can only pull from one of these existing classes. You can make Pegasus a Bird, but then it won’t be able to Whinny() or Gallop(). You can make it a Horse, but then it won’t be able to Fly().

Your first solution is to copy the Fly() method into the Pegasus class and derive Pegasus from Horse. This works fine, at the cost of having the Fly() method in two places (Bird and Pegasus). If you change one, you must remember to change the other. Of course, a developer who comes along months or years later to maintain your code must also know to fix both places.

Soon, however, you have a new problem. You want to create a list of Horse objects and a list of Bird objects. You’d like to be able to add your Pegasus objects to either list, but if a Pegasus is a Horse, you can’t add it to a list of Birds.

You have a couple of potential solutions. You can rename the Horse method Gallop() to Move(), and then override Move() in your Pegasus object to do the work of Fly(). You would then override Move() in your other horses to do the work of Gallop(). Perhaps Pegasus could be clever enough to gallop short distances and fly longer distances.


Pegasus::Move(long distance)
{
     if (distance > veryFar)
        fly(distance);
     else
        gallop(distance);
}

This is a bit limiting. Perhaps one day Pegasus will want to fly a short distance or gallop a long distance. Your next solution might be to move Fly() up into Horse, as illustrated in Listing 14.1. The problem is that most horses can’t fly, so you have to make this method do nothing unless it is a Pegasus.

Listing 14.1. If Horses Could Fly…

Image

Image


(1)Horse (2)Pegasus: 1
(1)Horse (2)Pegasus: 2
(1)Horse (2)Pegasus: 1
(1)Horse (2)Pegasus: 2
(1)Horse (2)Pegasus: 1

Horses can’t fly.
I can fly! I can fly! I can fly!
Horses can’t fly.
I can fly! I can fly! I can fly!
Horses can’t fly.

Image

This program certainly works, although at the expense of the Horse class having a Fly() method. On line 10, the method Fly() is provided to Horse. In a real-world class, you might have it issue an error, or fail quietly. On line 18, the Pegasus class overrides the Fly() method to “do the right thing,” represented here by printing a happy message.

The array of Horse pointers called Ranch on line 25 is used to demonstrate that the correct Fly() method is called, based on the runtime binding of the Horse or Pegasus object.

In lines 28–37, the user is prompted to select a Horse or a Pegasus. An object of the corresponding type is then created and placed into the Ranch array.

In lines 38–43, the program loops again through the Ranch array. This time, each object in the array has its Fly() method called. Depending on whether the object is a Horse or a Pegasus, the correct Fly() method is called. You can see this in the output. Because this program will no longer use the objects in Ranch, in line 42 a call to delete is made to free the memory used by each object.

Note

These examples have been stripped down to their bare essentials to illustrate the points under consideration. Constructors, virtual destructors, and so on have been removed to keep the code simple. This is not recommended for your programs.

Percolating Upward

Putting the required function higher in the class hierarchy is a common solution to this problem and results in many functions “percolating up” into the base class. The base class is then in grave danger of becoming a global namespace for all the functions that might be used by any of the derived classes. This can seriously undermine the class typing of C++, and can create a large and cumbersome base class.

In general, you want to percolate shared functionality up the hierarchy, without migrating the interface of each class. This means that if two classes that share a common base class (for example, Horse and Bird both share Animal) and have a function in common (both birds and horses eat, for example), you’ll want to move that functionality up into the base class and create a virtual function.

What you’ll want to avoid, however, is percolating a function (such as Fly) up where it doesn’t belong just so you can call that function only on some derived classes, when it doesn’t fit the meaning of that base class.

Casting Down

An alternative to this approach, still within single inheritance, is to keep the Fly() method within Pegasus and only call it if the pointer is actually pointing to a Pegasus object. To make this work, you need to be able to ask your pointer what type it is really pointing to. This is known as Run Time Type Identification (RTTI).

Because RTTI is a newer feature of the C++ specification, not all compilers support it. If your compiler does not support RTTI, you can mimic it by putting a method that returns an enumerated type in each of the classes. You can then test that type at runtime and call Fly() if it returns Pegasus.

Caution

Beware of using RTTI in your programs. Needing to use it might be an indication of poor inheritance hierarchy design. Consider using virtual functions, templates, or multiple inheritance instead.

In the previous example, you declared both Horse and Pegasus objects and placed them in an array of Horse objects. Everything was placed as a Horse. With RTTI, you would check each of these Horses to see if it was just a horse or if indeed a Pegasus had actually been created.

To call Fly(), however, you must cast the pointer, telling it that the object it is pointing to is a Pegasus object, not a Horse. This is called casting down because you are casting the Horse object down to a more derived type.

C++ now officially, though perhaps reluctantly, supports casting down using the new dynamic_cast operator. Here’s how it works.

If you have a pointer to a base class such as Horse, and you assign to it a pointer to a derived class, such as Pegasus, you can use the Horse pointer polymorphically. If you then need to get at the Pegasus object, you create a Pegasus pointer and use the dynamic_cast operator to make the conversion.

At runtime, the base pointer is examined. If the conversion is proper, your new Pegasus pointer is fine. If the conversion is improper, if you didn’t really have a Pegasus object after all, then your new pointer is null. Listing 14.2 illustrates this point.

Listing 14.2. Casting Down

Image

Image

Image


(1)Horse (2)Pegasus: 1
(1)Horse (2)Pegasus: 2
(1)Horse (2)Pegasus: 1
(1)Horse (2)Pegasus: 2
(1)Horse (2)Pegasus: 1

Just a horse
I can fly! I can fly! I can fly!
Just a horse
I can fly! I can fly! I can fly!
Just a horse

FAQ

When compiling, I got a warning from Microsoft Visual C++: “warning C4541: ‘dynamic_cast’ used on polymorphic type ‘class Horse’ with /GR-; unpredictable behavior may result.” What should I do? When running this program, I get a message:

“This application has requested the Runtime to terminate it in an unusual way. Please contact the application’s support team for more information.”

Answer: These are some of this compiler’s most confusing error messages. To fix these, do the following:

1. In your project, choose Project, Settings.

2. Go to the C++ tab.

3. Change the drop-down to C++ Language.

4. Click Enable Runtime Type Information (RTTI).

5. Rebuild your entire project.

Alternatively, if you are using the command-line compiler for Visual C++, add the /GR flag:


cl /GR List1402.cpp

Image

This solution also works; however, it is not recommended.

The desired results are achieved. Fly() is kept out of Horse, and it is not called on Horse objects. When it is called on Pegasus objects (line 45), however, the objects must be explicitly cast (line 43); Horse objects don’t have the method Fly(), so the pointer must be told it is pointing to a Pegasus object before being used.

The need for you to cast the Pegasus object is a warning that something might be wrong with your design. This program effectively undermines the virtual function polymorphism because it depends on casting the object to its real runtime type.

Adding to Two Lists

The other problem with these solutions is that you’ve declared Pegasus to be a type of Horse, so you cannot add a Pegasus object to a list of Birds. You’ve paid the price of either moving Fly() up into Horse or casting down the pointer, and yet you still don’t have the full functionality you need.

One final, single inheritance solution presents itself. You can push Fly(), Whinny(), and Gallop() all up into a common base class of both Bird and Horse: Animal. Now, instead of having a list of Birds and a list of Horses, you can have one unified list of Animals. This works, but eventually leads to a base class that has all of the characteristics of all of its descendant classes. So who needs descendant classes then?

Alternatively, you can leave the methods where they are and cast down Horses and Birds and Pegasus objects, but that is even worse!

Image

Multiple Inheritance

It is possible to derive a new class from more than one base class. This is called multiple inheritance. To derive from more than the base class, you separate each base class by commas in the class designation, as shown here:


class DerivedClass : public BaseClass1, public BaseClass2 {}

This is exactly like declaring single inheritance with an additional base class, BaseClass2, added.

Listing 14.3 illustrates how to declare Pegasus so that it derives from both Horses and Birds. The program then adds Pegasus objects to both types of lists.

Listing 14.3. Multiple Inheritance

Image

Image

Image



(1)Horse (2)Pegasus: 1
Horse constructor...
(1)Horse (2)Pegasus: 2
Horse constructor... Bird constructor... Pegasus constructor...
(1)Bird (2)Pegasus: 1
Bird constructor...
(1)Bird (2)Pegasus: 2
Horse constructor... Bird constructor... Pegasus constructor...


Ranch[0]: Whinny!... Horse destructor...
Ranch[1]: Whinny!... Pegasus destructor...  Bird destructor...
Horse destructor...
Aviary[0]: Chirp... I can fly! I can fly! I can fly! Bird destructor...
Aviary[1]: Whinny!... I can fly! I can fly! I can fly!
Pegasus destructor... Bird destructor... Horse destructor...

Image

On lines 7–15, a Horse class is declared. The constructor and destructor print out a message, and the Whinny() method prints Whinny!....

On lines 17–29, a Bird class is declared. In addition to its constructor and destructor, this class has two methods: Chirp() and Fly(), both of which print identifying messages. In a real program, these might, for example, activate the speaker or generate animated images.

Finally, on lines 31–37, you see the new code—using multiple inheritance, the class Pegasus is declared. In line 31, you can see that this class is derived from both Horse and Bird. The Pegasus class overrides the Chirp() method in line 34. The PegasusChirp() method simply does a call to the Whinny() method, which it inherits from Horse.

In the main section of this program, two lists are created: a Ranch with pointers to Horse objects on line 42, and an Aviary with pointers to Bird objects on line 43. On lines 47–56, Horse and Pegasus objects are added to the Ranch. On lines 57–66, Bird and Pegasus objects are added to the Aviary.

Invocations of the virtual methods on both the Bird pointers and the Horse pointers do the right things for Pegasus objects. For example, on line 79 the members of the Aviary array are used to call Chirp() on the objects to which they point. The Bird class declares this to be a virtual method, so the right function is called for each object.

Note that each time a Pegasus object is created, the output reflects that both the Bird part and the Horse part of the Pegasus object are also created. When a Pegasus object is destroyed, the Bird and Horse parts are destroyed as well, thanks to the destructors being made virtual.

Declaring Multiple Inheritance

Declare an object to inherit from more than one class by listing the base classes following the colon after the class name. Separate the base classes by commas.

Example 1


class Pegasus : public Horse, public Bird

Example 2


class Schnoodle : public Schnauzer, public Poodle

The Parts of a Multiply Inherited Object

When the Pegasus object is created in memory, both the base classes form part of the Pegasus object, as illustrated in Figure 14.1. This figure represents an entire Pegasus object. This includes the new features added in the Pegasus class and the features picked up from the base classes.

Figure 14.1. Multiply inherited objects.

Image

Several issues arise with objects with multiple base classes. For example, what happens if two base classes that happen to have the same name have virtual functions or data? How are multiple base class constructors initialized? What happens if multiple base classes both derive from the same class? The next sections answer these questions and explore how multiple inheritance can be put to work.

Constructors in Multiply Inherited Objects

If Pegasus derives from both Horse and Bird, and each of the base classes has constructors that take parameters, the Pegasus class initializes these constructors in turn. Listing 14.4 illustrates how this is done.

Listing 14.4. Calling Multiple Constructors

Image

Image

Image

Image


Horse constructor...
Bird constructor...
Pegasus constructor...
I can fly! I can fly! I can fly! Whinny!...
Your Pegasus is 5 hands tall and it does migrate.
A total of 10 people believe it exists.
Pegasus destructor...
Bird destructor...
Horse destructor...

Image

On lines 9–20, the Horse class is declared. The constructor takes two parameters: One is an enumeration for colors, which is declared on line 7, and the other is a typedef declared on line 6. The implementation of the constructor on lines 22–26 simply initializes the member variables and prints a message.

On lines 28–44, the Bird class is declared, and the implementation of its constructor is on lines 46–50. Again, the Bird class takes two parameters. Interestingly, the Horse constructor takes color (so that you can detect horses of different colors), and the Bird constructor takes the color of the feathers (so those of one feather can stick together). This leads to a problem when you want to ask the Pegasus for its color, which you’ll see in the next example.

The Pegasus class itself is declared on lines 52–65, and its constructor is on lines 67–77. The initialization of the Pegasus object includes three statements. First, the Horse constructor is initialized with color and height (line 72). Then, the Bird constructor is initialized with color and the Boolean indicating if it migrates (line 73). Finally, the Pegasus member variable itsNumberBelievers is initialized. After all that is accomplished, the body of the Pegasus constructor is called.

In the main() function, a Pegasus pointer is created in line 81. This object is then used to access the member functions that were derived from the base classes. The access of these methods is straightforward.

Ambiguity Resolution

In Listing 14.4, both the Horse class and the Bird class have a method GetColor(). You’ll notice that these methods were not called in Listing 14.4! You might need to ask the Pegasus object to return its color, but you have a problem—the Pegasus class inherits from both Bird and Horse. They both have a color, and their methods for getting that color have the same names and signature. This creates an ambiguity for the compiler, which you must resolve.

If you simply write


COLOR currentColor = pPeg->GetColor();

you receive a compiler error:


Member is ambiguous: ’Horse::GetColor’ and ’Bird::GetColor’

You can resolve the ambiguity with an explicit call to the function you want to invoke:


COLOR currentColor = pPeg->Horse::GetColor();

Any time you need to resolve which class a member function or member data inherits from, you can fully qualify the call by prepending the class name to the base class data or function.

Note that if Pegasus were to override this function, the problem would be moved, as it should be, into the Pegasus member function:


virtual COLOR GetColor()const { return Horse::GetColor(); }

This hides the problem from clients of the Pegasus class and encapsulates within Pegasus the knowledge of which base class from which it wants to inherit its color. A client is still free to force the issue by writing


COLOR currentColor = pPeg->Bird::GetColor();

Inheriting from Shared Base Class

What happens if both Bird and Horse inherit from a common base class, such as Animal? Figure 14.2 illustrates what this looks like.

Figure 14.2. Common base classes.

Image

As you can see in Figure 14.2, two base class objects exist. When a function or data member is called in the shared base class, another ambiguity exists. For example, if Animal declares itsAge as a member variable and GetAge() as a member function, and you call pPeg->GetAge(), did you mean to call the GetAge() function you inherit from Animal by way of Horse, or by way of Bird? You must resolve this ambiguity as well, as illustrated in Listing 14.5.

Listing 14.5. Common Base Classes

Image

Image

Image

Image


Animal constructor...
Horse constructor...
Animal constructor...
Bird constructor...
Pegasus constructor...
This pegasus is 2 years old.
Pegasus destructor...
Bird destructor...
Animal destructor...
Horse destructor...
Animal destructor...

Image

Several interesting features are in this listing. The Animal class is declared on lines 9–18. Animal adds one member variable, itsAge, and two accessors: GetAge() and SetAge().

On line 26, the Horse class is declared to derive from Animal. The Horse constructor now has a third parameter, age, which it passes to its base class, Animal (see line 40). Note that the Horse class does not override GetAge(), it simply inherits it.

On line 46, the Bird class is declared to derive from Animal. Its constructor also takes an age and uses it to initialize its base class, Animal (see line 62). It also inherits GetAge() without overriding it.

Pegasus inherits from both Bird and Horse in line 68, and so has two Animal classes in its inheritance chain. If you were to call GetAge() on a Pegasus object, you would have to disambiguate, or fully qualify, the method you want if Pegasus did not override the method.

This is solved on line 77 when the Pegasus object overrides GetAge() to do nothing more than to chain up—that is, to call the same method in a base class.

Chaining up is done for two reasons: either to disambiguate which base class to call, as in this case, or to do some work and then let the function in the base class do some more work. At times, you might want to do work and then chain up, or chain up and then do the work when the base class function returns.

The Pegasus constructor, which starts on line 82, takes five parameters: the creature’s color, its height (in HANDS), whether it migrates, how many believe in it, and its age.

The constructor initializes the Horse part of the Pegasus with the color, height, and age on line 88. It initializes the Bird part with color, whether it migrates, and age on line 89. Finally, it initializes itsNumberBelievers on line 90.

The call to the Horse constructor on line 88 invokes the implementation shown on line 39. The Horse constructor uses the age parameter to initialize the Animal part of the Horse part of the Pegasus. It then goes on to initialize the two member variables of HorseitsColor and itsHeight.

The call to the Bird constructor on line 89 invokes the implementation shown on line 61. Here, too, the age parameter is used to initialize the Animal part of the Bird.

Note that the color parameter to the Pegasus is used to initialize member variables in each of Bird and Horse. Note also that the age is used to initialize itsAge in the Horse’s base Animal and in the Bird’s base Animal.

Caution

Keep in mind that whenever you explicitly disambiguate an ancestor class, you create a risk that a new class inserted between your class and its ancestor will cause this class to inadvertently call “past” the new ancestor into the old ancestor, and this can have unexpected effects.

Virtual Inheritance

In Listing 14.5, the Pegasus class went to some lengths to disambiguate which of its Animal base classes it meant to invoke. Most of the time, the decision as to which one to use is arbitrary—after all, the Horse and the Bird have the same base class.

It is possible to tell C++ that you do not want two copies of the shared base class, as shown in Figure 14.2, but rather to have a single shared base class, as shown in Figure 14.3.

Figure 14.3. A diamond inheritance.

Image

You accomplish this by making Animal a virtual base class of both Horse and Bird. The Animal class does not change at all. The Horse and Bird classes change only in their use of the term virtual in their declarations. Pegasus, however, changes substantially.

Normally, a class’s constructor initializes only its own variables and its base class. Virtually inherited base classes are an exception, however. They are initialized by their most derived class. Thus, Animal is initialized not by Horse and Bird, but by Pegasus. Horse and Bird have to initialize Animal in their constructors, but these initializations will be ignored when a Pegasus object is created.

Listing 14.6 rewrites Listing 14.5 to take advantage of virtual derivation.

Listing 14.6. Illustration of the Use of Virtual Inheritance

Image

Image

Image

Image


Animal constructor...
Horse constructor...
Bird constructor...
Pegasus constructor...
This pegasus is 4 years old.
Pegasus destructor...
Bird destructor...
Horse destructor...
Animal destructor...

Image

On line 25, Horse declares that it inherits virtually from Animal, and on line 45, Bird makes the same declaration. Note that the constructors for both Bird and Animal still initialize the Animal object.

Pegasus inherits from both Bird and Animal, and as the most derived object of Animal, it also initializes Animal. It is Pegasus’s initialization which is called, however, and the calls to Animal’s constructor in Bird and Horse are ignored. You can see this because the value 2 is passed in, and Horse and Bird pass it along to Animal, but Pegasus doubles it. The result, 4, is reflected in the output generated from line 98.

Pegasus no longer has to disambiguate the call to GetAge(), and so is free to simply inherit this function from Animal. Note that Pegasus must still disambiguate the call to GetColor() because this function is in both of its base classes and not in Animal.

Declaring Classes for Virtual Inheritance

To ensure that derived classes have only one instance of common base classes, declare the intermediate classes to inherit virtually from the base class.

Example 1


class Horse : virtual public Animal
class Bird : virtual public Animal
class Pegasus : public Horse, public Bird

Example 2


class Schnauzer : virtual public Dog
class Poodle : virtual public Dog
class Schnoodle : public Schnauzer, public Poodle

Problems with Multiple Inheritance

Although multiple inheritance offers several advantages over single inheritance, many C++ programmers are reluctant to use it. The problems they cite are that it makes debugging harder, that evolving multiple inheritance class hierarchies is harder and more risky than evolving single inheritance class hierarchies, and that nearly everything that can be done with multiple inheritance can be done without it. Other languages, such as Java and C#, don’t support multiple inheritance of classes for some of these same reasons.

These are valid concerns, and you will want to be on your guard against installing needless complexity into your programs. Some debuggers have a hard time with multiple inheritance, and some designs are needlessly made complex by using multiple inheritance when it is not needed.

Image

Mixins and Capabilities Classes

One way to strike a middle ground between multiple inheritance and single inheritance is to use what are called mixins. Thus, you might have your Horse class derive from Animal and from Displayable. Displayable would just add a few methods for displaying any object onscreen.

A mixin, or capability class, is a class that adds specialized functionality without adding many additional methods or much data.

Capability classes are mixed into a derived class the same as any other class might be, by declaring the derived class to inherit publicly from them. The only difference between a capability class and any other class is that the capability class has little or no data. This is an arbitrary distinction, of course, and is just a shorthand way of noting that at times all you want to do is mix in some additional capabilities without complicating the derived class.

This will, for some debuggers, make it easier to work with mixins than with more complex multiply inherited objects. In addition, less likelihood exists of ambiguity in accessing the data in the other principal base class.

For example, if Horse derives from Animal and from Displayable, Displayable would have no data. Animal would be just as it always was, so all the data in Horse would derive from Animal, but the functions in Horse would derive from both.

Note

The term mixin comes from an ice cream store in Sommerville, Massachusetts, where candies and cakes were mixed into the basic ice cream flavors. This seemed like a good metaphor to some of the object-oriented programmers who used to take a summer break there, especially while working with the object-oriented programming language SCOOPS.

Abstract Data Types

Often, you will create a hierarchy of classes together. For example, you might create a Shape class, and derive from that Rectangle and Circle. From Rectangle, you might derive Square as a special case of Rectangle.

Each of the derived classes will override the Draw() method, the GetArea() method, and so forth. Listing 14.7 illustrates a bare-bones implementation of the Shape class and its derived Circle and Rectangle classes.

Listing 14.7. Shape Classes

Image

Image

Image

Image


(1)Circle (2)Rectangle (3)Square (0)Quit: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x

(1)Circle (2)Rectangle (3)Square (0)Quit:3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x

(1)Circle (2)Rectangle (3)Square (0)Quit:0

0

Image

On lines 7–16, the Shape class is declared. The GetArea() and GetPerim() methods return an error value, and Draw() takes no action. After all, what does it mean to draw a Shape? Only types of shapes (circles, rectangles, and so on) can be drawn; Shapes as an abstraction cannot be drawn.

Circle derives from Shape in lines 18–29 and overrides the three virtual methods. Note that no reason exists to add the word “virtual,” because that is part of their inheritance. But there is no harm in doing so either, as shown in the Rectangle class on lines 43, 44, and 47. It is a good idea to include the term virtual as a reminder, a form of documentation.

Square derives from Rectangle in lines 64–71, and it, too, overrides the GetPerim() method, inheriting the rest of the methods defined in Rectangle.

It is troubling, though, that a client might try to instantiate a Shape, and it might be desirable to make that impossible. After all, the Shape class exists only to provide an interface for the classes derived from it; as such, it is an abstract data type, or ADT.

In an abstract class, the interface represents a concept (such as shape) rather than a specific object (such as circle). In C++, an abstract class is always the base class to other classes, and it is not valid to make an instance of an abstract class.

Pure Virtual Functions

C++ supports the creation of abstract classes by providing the pure virtual function. A virtual function is made pure by initializing it with zero, as in


virtual void Draw() = 0;

In this example, the class has a Draw() function, but it has a null implementation and cannot be called. It can, however, be overwritten within descendant classes.

Any class with one or more pure virtual functions is an abstract class, and it becomes illegal to instantiate. In fact, it is illegal to instantiate an object of any class that is an abstract class or any class that inherits from an abstract class and doesn’t implement all of the pure virtual functions. Trying to do so causes a compile-time error. Putting a pure virtual function in your class signals two things to clients of your class:

• Don’t make an object of this class; derive from it.

• Be certain to override the pure virtual functions your class inherits.

Any class that derives from an abstract class inherits the pure virtual function as pure, and so must override every pure virtual function if it wants to instantiate objects. Thus, if Rectangle inherits from Shape, and Shape has three pure virtual functions, Rectangle must override all three or it, too, will be an abstract class. Listing 14.8 rewrites the Shape class to be an abstract data type. To save space, the rest of Listing 14.7 is not reproduced here. Replace the declaration of Shape in Listing 14.7, lines 7–16, with the declaration of Shape in Listing 14.8 and run the program again.

Listing 14.8. Abstract Class


0:    //Listing 14.8 Abstract Classes
1:  
2:    class Shape
3:    {
4:      public:
5:         Shape(){}
6:         ~Shape(){}
7:         virtual long GetArea() = 0;
8:         virtual long GetPerim()= 0;
9:         virtual void Draw() = 0;
10:      private:
11:    };

Image


(1)Circle (2)Rectangle (3)Square (0)Quit: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x

(1)Circle (2)Rectangle (3)Square (0)Quit: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x

(1)Circle (2)Rectangle (3)Square (0)Quit: 0

Image

As you can see, the workings of the program are totally unaffected. The only difference is that it would now be impossible to make an object of class Shape.

Abstract Data Types

Declare a class to be an abstract class (also called an abstract data type) by including one or more pure virtual functions in the class declaration. Declare a pure virtual function by writing = 0 after the function declaration.

Example


class Shape
{
     virtual void Draw() = 0;    // pure virtual
};

Implementing Pure Virtual Functions

Typically, the pure virtual functions in an abstract base class are never implemented. Because no objects of that type are ever created, no reason exists to provide implementations, and the abstract class works purely as the definition of an interface to objects, which derive from it.

It is possible, however, to provide an implementation to a pure virtual function. The function can then be called by objects derived from the abstract class, perhaps to provide common functionality to all the overridden functions. Listing 14.9 reproduces Listing 14.7, this time with Shape as an abstract class and with an implementation for the pure virtual function Draw(). The Circle class overrides Draw(), as it must, but it then chains up to the base class function for additional functionality.

In this example, the additional functionality is simply an additional message printed, but one can imagine that the base class provides a shared drawing mechanism, perhaps setting up a window that all derived classes will use.

Listing 14.9. Implementing Pure Virtual Functions

Image

Image

Image

Image


(1)Circle (2)Rectangle (3)Square (0)Quit: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x
Abstract drawing mechanism!


(1)Circle (2)Rectangle (3)Square (0)Quit: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
Abstract drawing mechanism!

(1)Circle (2)Rectangle (3)Square (0)Quit: 0

Image

On lines 5–14, the abstract class Shape is declared, with all three of its accessor methods declared to be pure virtual. Note that this is not necessary, but is still a good practice. If any one were declared pure virtual, the class would have been an abstract class.

The GetArea() and GetPerim() methods are not implemented, but Draw() is implemented in lines 16–19. Circle and Rectangle both override Draw(), and both chain up to the base method, taking advantage of shared functionality in the base class.

Complex Hierarchies of Abstraction

At times, you will derive abstract classes from other abstract classes. It might be that you will want to make some of the derived pure virtual functions nonpure, and leave others pure.

If you create the Animal class, you can make Eat(), Sleep(), Move(), and Reproduce() all be pure virtual functions. Perhaps from Animal you derive Mammal and Fish.

On examination, you decide that every Mammal will reproduce in the same way, and so you make Mammal::Reproduce() nonpure, but you leave Eat(), Sleep(), and Move() as pure virtual functions.

From Mammal, you derive Dog, and Dog must override and implement the three remaining pure virtual functions so that you can make objects of type Dog.

What you’ve said, as class designer, is that no Animals or Mammals can be instantiated, but that all Mammals can inherit the provided Reproduce() method without overriding it.

Listing 14.10 illustrates this technique with a bare-bones implementation of these classes.

Listing 14.10. Deriving Abstract Classes from Other Abstract Classes

Image

Image

Image

Image


(1)Dog (2)Horse (3)Bird (0)Quit: 1
Animal constructor...
Mammal constructor...
Dog constructor...
Whoof!...
Dog eating...
Dog reproducing....
Dog running...
Dog snoring...
Dog destructor...
Mammal destructor...
Animal destructor...

(1)Dog (2)Horse (3)Bird (0)Quit: 0

Image

On lines 7–21, the abstract class Animal is declared. Animal has nonpure virtual accessors for itsAge, which are shared by all Animal objects. It has five pure virtual functions, Sleep(), Eat(), Reproduce(), Move(), and Speak().

Mammal is derived from Animal on lines 29–37, and adds no data. It overrides Reproduce(), however, providing a common form of reproduction for all mammals. Fish must override Reproduce() because Fish derives directly from Animal and cannot take advantage of Mammalian reproduction (and a good thing, too!). Fish does this in lines 47–48.

Mammal classes no longer have to override the Reproduce() function, but they are free to do so if they choose, as Dog does on line 83. Fish, Horse, and Dog all override the remaining pure virtual functions, so that objects of their type can be instantiated.

In the body of the main program, an Animal pointer is used to point to the various derived objects in turn. The virtual methods are invoked, and based on the runtime binding of the pointer, the correct method is called in the derived class.

It would be a compile-time error to try to instantiate an Animal or a Mammal, as both are abstract classes.

Which Classes Are Abstract?

In one program, the class Animal is abstract; in another, it is not. What determines whether to make a class abstract?

The answer to this question is decided not by any real-world intrinsic factor, but by what makes sense in your program. If you are writing a program that depicts a farm or a zoo, you might want Animal to be an abstract class, but Dog to be a class from which you can instantiate objects.

On the other hand, if you are making an animated kennel, you might want to keep Dog as an abstract class and only instantiate types of dogs: retrievers, terriers, and so forth. The level of abstraction is a function of how finely you need to distinguish your types.

Image

Summary

Today, you learned how to overcome some of the limitations in single inheritance. You learned about the danger of percolating functions up the inheritance hierarchy and the risks in casting down the inheritance hierarchy. You also learned how to use multiple inheritance, what problems multiple inheritance can create, and how to solve them using virtual inheritance.

You also learned what abstract classes are and how to create abstract classes using pure virtual functions. You learned how to implement pure virtual functions and when and why you might do so.

Q&A

Q   What is the v-ptr?

A   The v-ptr, or virtual-function pointer, is an implementation detail of virtual functions. Each object in a class with virtual functions has a v-ptr, which points to the virtual function table for that class. The virtual function table is consulted when the compiler needs to determine which function to call in a particular situation.

Q   Is percolating upward always a good thing?

A   Yes, if you are percolating shared functionality upward. No, if all you are moving is interface. That is, if all the derived classes can’t use the method, it is a mistake to move it up into a common base class. If you do, you’ll have to switch on the runtime type of the object before deciding if you can invoke the function.

Q   Why is making a decision on the runtime type of an object bad?

A   Because this is an indication that the inheritance hierarchy for the class has not been properly constructed, and it is better to go back and fix the design than to use this workaround.

Q   Why is casting bad?

A   Casting isn’t bad if it is done in a way that is type-safe. Casting can, however, be used to undermine the strong type checking in C++, and that is what you want to avoid. If you are switching on the runtime type of the object and then casting a pointer, that might be a warning sign that something is wrong with your design.

In addition, functions should work with the declared type of their arguments and member variables, and not depend on “knowing” what the calling program will provide through some sort of implicit contract. If the assumption turns out to be wrong, strange and unpredictable problems can result.

Q   Why not make all functions virtual?

A   Virtual functions are supported by a virtual function table, which incurs runtime overhead, both in the size of the program and in the performance of the program. If you have very small classes that you don’t expect to subclass, you might not want to make any of the functions virtual. However, when this assumption changes, you need to be careful to go back and make the ancestor class functions virtual, or unexpected problems can result.

Q   When should the destructor be made virtual?

A   The destructor should be made virtual any time you think the class will be subclassed, and a pointer to the base class will be used to access an object of the subclass. As a general rule of thumb, if you’ve made any functions in your class virtual, be certain to make the destructor virtual as well.

Q   Why bother making an abstract class—why not just make it nonabstract and avoid creating any objects of that type?

A   The purpose of many of the conventions in C++ is to enlist the compiler in finding bugs, so as to avoid runtime bugs in code that you give your customers. Making a class abstract—that is, giving it pure virtual functions—causes the compiler to flag any objects created of that abstract type as errors.

Workshop

The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix D, and be certain you understand the answers before continuing to tomorrow’s lesson.

Quiz

1. What is a down cast?

2. What does “percolating functionality upward” mean?

3. If a round-rectangle has straight edges and rounded corners, and your RoundRect class inherits both from Rectangle and from Circle, and they in turn both inherit from Shape, how many Shapes are created when you create a RoundRect?

4. If Horse and Bird inherit from Animal using public virtual inheritance, do their constructors initialize the Animal constructor? If Pegasus inherits from both Horse and Bird, how does it initialize Animal’s constructor?

5. Declare a class called Vehicle and make it an abstract class.

6. If a base class is an abstract class, and it has three pure virtual functions, how many of these must be overridden in its derived classes?

Exercises

1. Show the declaration for a class JetPlane, which inherits from Rocket and Airplane.

2. Show the declaration for Seven47, which inherits from the JetPlane class described in Exercise 1.

3. Write the code that derives Car and Bus from the class Vehicle. Make Vehicle be an abstract class with two pure virtual functions. Make Car and Bus not be abstract classes.

4. Modify the code in Exercise 3 so that Car is an abstract class, and derive SportsCar and Coupe from Car. In the Car class, provide an implementation for one of the pure virtual functions in Vehicle and make it nonpure.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.179.100