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
Suppose you’ve been working with your animal classes for a while, and you’ve divided the class hierarchy into Bird
s and Mammal
s. The Bird
class includes the member function Fly()
. The Mammal
class has been divided into a number of types of Mammal
s, 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 Bird
s.
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…
(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.
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.
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.
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 Horse
s 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
(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
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.
cl /GR List1402.cpp
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.
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 Bird
s. 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 Bird
s and a list of Horse
s, you can have one unified list of Animal
s. 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 Horse
s and Bird
s and Pegasus
objects, but that is even worse!
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 Horse
s and Bird
s. The program then adds Pegasus
objects to both types of lists.
Listing 14.3. Multiple Inheritance
(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...
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 Pegasus
’ Chirp()
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.
class Pegasus : public Horse, public Bird
class Schnoodle : public Schnauzer, public Poodle
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.
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.
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
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...
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.
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();
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.
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
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...
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 Horse
—itsColor
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.
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.
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
Animal constructor...
Horse constructor...
Bird constructor...
Pegasus constructor...
This pegasus is 4 years old.
Pegasus destructor...
Bird destructor...
Horse destructor...
Animal destructor...
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
.
class Horse : virtual public Animal
class Bird : virtual public Animal
class Pegasus : public Horse, public Bird
class Schnauzer : virtual public Dog
class Poodle : virtual public Dog
class Schnoodle : public Schnauzer, public Poodle
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.
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.
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
(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
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.
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: };
(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
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
.
class Shape
{
virtual void Draw() = 0; // pure virtual
};
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
(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
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.
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 Animal
s or Mammal
s can be instantiated, but that all Mammal
s 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
(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
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.
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.
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 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.
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.
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 Shape
s 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?
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.
18.226.181.57