It Is a Profoundly Erroneous Truism, Repeated by All Copy Books and by Eminent People

It Is a Profoundly Erroneous Truism, Repeated by All Copy Books and by Eminent People

Exercises1 of 13

9

Polymorphism

It is a profoundly erroneous truism, repeated by all copy books and by eminent people when they are making speeches, that we should cultivate the habit of thinking of what we are doing. The precise opposite is the case. Civilization advances by extending the number of important operations which we can perform without thinking about them.

Alfred North Whitehead

An Introduction to Mathematics

Polymorphism literally means many forms (from the Greek for many forms, by way of Latin). There are, as it happens, many forms of polymorphism in C++: overloading by parameters, default function values, and the use of overridden, often virtual functions. We’ll be talking mainly about the third variety.

The big strength of virtual functions is that the caller doesn’t need to know the real, underlying type of the object on which the member function is being invoked. The caller need only know the abstract class that is being extended, the interface this class implements, the API for the black box in question.

Polymorphism with shapes

Suppose we have several classes representing shapes—Circle, Square, Rectangle—which all extend a Shape class, whose interface contains an area() function (standardize accessors with or without get?). I were not using classes—if Shape were a mere struct, which represents different kinds of shapes according to its type member—it might seem natural to centralize the intelligence for all versions of area() in a single function. The following would be a reasonable implementation:

double area(const Shape &s) {

switch (s.type) {

case CIRCLE:

return s.rad*s.rad*PI;

case SQUARE:

return s.side*s.side;

case RECT:

return s.length*s.width;

default:

return -1;

}

In moving to classes, then, a natural translation is to place this function within the parent Shape class:

double Shape::area() {

switch (s.getType()) {

case CIRCLE:

return this->getRad()*this->getRad()*PI;

case SQUARE:

return this->getSide()*this->getSide();

case RECT:

return this->getLength()*this->getWidth();

default:

return -1;

}

(this-> may be omitted, but it’s included in order to make explicit the logic of the translation.) The implied strategy for the development of the child classes is that each child constructor will set the type member appropriately, with the result that area() manifests the child-specific behavior.

Of course, this strategy will work, but it ignores much of the strength of object-orientation. Specifically, it fails to take advantage of polymorphism.

Let’s start over. We’ve got a generic Shape class, and its children are Circle, Square, Rectangle. Because these are separate classes, the type member is redundant. Type information is already stored in the class definition itself. This doesn’t mean we should replace the switch statement with if statements examining typeid(s).name(), although that would work, too, and would be a marginal improvement. But we don’t need to examine the type information al all, in any of its forms. The compiler will do this for us. When we invoke a function, after all, it’s the compiler’s responsibility to call the right function, based on the types of the arguments we pass and of the object on which the function in invoked. The solution, therefore, is to write a function for each possible object type—each Shape—and the compiler will then call it for us. This, incidentally, is the sort of thing Whitehead was talking about. In general, it’s a Bad Idea (barring a good reason) to do something explicitly that the language will do for you implicitly.

Instead, the area function area will become a virtual function in the Shape class. Because there is no shared behavior between the area functions of all shapes, though, we would ordinarily have to define Shape::area() as an empty function, created to be overridden. Instead, we can define it as a pure virtual function, with the following code in the class definition:

virtual double area() = 0;

No mention of area() is then required in the Shape implementation (if there is one). An implementation is now required in the child class implementations, e.g.:

Double Circle::area() {

return PI*getRad()*getRad();

}

Intuitively, Shape::area() is set to the null value, until it’s overridden. (is a declare req-ed in the child class definition?)

In a sense, we’re joining together shared intelligence, the old area function was doing all the area computation, but this is true only semantically (find term). In a more relevant sense, this intelligence is manifestly unshared—it’s three separate functions artificially joined by a switch. This intelligence consists of three different, unshared functions, gathered together from three different classes, so it’s poor design to place it together in a single function. It’s better design to spend the four function headers and put each of them in a class-specific function implementation and to write function reader in Shape. In the future, when we create the class Parallelogram, we won’t have to go back and modify Shape (even if we have access to it by then), risking an effect on our existing Shape classes, but only implement the Shape interface in our new class. Encapsulation is about each class being responsible for itself and only for itself.

Abstract classes

An abstract class A is a class with virtual functions. What this means is that either the class itself or one of its ancestors defines a virtual function that has not been implemented (by the class itself or one of its ancestors). The class is incomplete, so it can’t be instantiated. Nonetheless, an object might be an instance of this abstract class, if it’s the result of instantiating a (non-abstract) descendent of it.

Varieties of abstraction

Just as a class can be viewed as an abstraction from particular objects, an abstract class is like an abstraction from particular classes. A class is a abstraction from particular objects, generalizing away from possible values of data members but retaining the data member types and both the member function interfaces and implementations. An abstract class is an abstraction from particular classes, generalizing from particular member function implementations. Later on, we’ll see that a template class is different kind of abstraction from classes, generalizing from particular data types. (Analogously, a template function abstracts from particular functions.)

Squares and rectangles

Let’s return to the concepts of square and rectangle. They’re very similar, almost the same, in fact, except that the square’s two dimensions are equal. So which class should extend the other? Maybe Rectangle should be the derived class. Then Square looks like:

class Square: public Shape {

public:

Square(double d): side(d) {}

double getArea() { retrn val*val; }

double getSide() { return side; }

private:

double side;

};

and Rectangle looks like:

class Rectangle: private Square {

public:

Rect(double d1, double d2): len(d1), wid(d2) {}

double getArea() { return len*wid; }

double getLength() { return len; }

double getWidth() { return wid; }

private:

double len, wid;

};

This would compile, but we have to ask whether the is-a relationship holds between Rectangle and Square? No, it’s not the case that all rectangles are squares, it’s the other way around. As should, therefore, our inheritance be:

class Square: public Shape {

public:

Square(double d): side(d1) {}

double getArea() { return side*side; }

double getSide() { return side; }

private:

double side;

};

Our rectangle class then becomes:

class Rectangle: private Square {

public:

Square(double d1, double d2): side(d1),wid(d2) {}

double getArea() { return side*wid; }

double getLength() { return Square::getSide(); }

double getWidth() { return width; }

private:

double wid;

};

This second way also works, and is arguably much better design, but it’s a little longer because it involves the reinterpretation of an existing data member. The Square’s side member is reused as one of the rectangle’s two length values. We could do public extension and grant access to this value with the existing Square::side() function, but we want the user to think of the value returned as one of the rectangle’s two side values—length, say—so “getSide” is a poor name. Instead, therefore, we use private extension so that we, inside Rectangle, have access to Square’s member functions, but users of Rectangle do not.

Now, side was declared in Square as private, so, regardless of how Rectangle extends Square, side will not be accessible inside of Rectangle. Our getLength() function, therefore, cannot be defined thus:

double getLength() { return side; }

This would compile only if Square::side had been protected. We can, though, call Square’s getSide() as a subroutine, which is what we now do. (is the Square:: necessary if extension is private and we don’t define redefine getSide here?) We don’t have a super keyword; instead, we specify the ancestor class whose version of the function we wish to call. (can we use this to call ftns >1 level up? Confirm with an explicit example)

Again, both models work, but the second is preferable. Plato said to “carve nature at its joints”, and this is finally the goal of designing object models. How to do it in particular cases is an act of judgment, but guiding principles include turning individual, discrete concepts into classes and placing the classes into a hierarchy in which the is-a relationship obtains, in the right way, in the case of each class extension.

So we can have either RectangleSquare xor SquareRectangle—that is, either class can extend the other, but not both, since the result would be circular. The class hierarchy is a tree.

On to polymorphism

The advantage of polymorphism can be particularly dramatic if we have a larger number of classes derived from a single abstract parent. Suppose we have an abstract Animal class (poly.cpp?):

class Animal {

public:

virtual void talk() = 0;

};

and several extensions, such as:

class Cat: public Animal {

public:

void talk() { cout < "Meow!\n"; }

};

For the following example, we’ll use a simple factory function for convenience (move into class as static function; maybe change to reference return):

Animal* getInst(int which) {

switch (which) {

case 0: return new Cat;

case 1: return new Dog;

case 2: return new Frog;

case 3: return new Human;

default:

cout < "bad i!\n";

return null;

}

}

Here’s the example itself:

srand(time(NULL)); // seed the RNG

Animal** ans = new Animal*[10]; // constr ar

for (int i = 0; i < 10; i++) // constr rand

ans[i] = getInst(rand() % 4); // insts

for (int i = 0; i < 10; i++)

ans[i]->talk(); // each inst talks

for (int i = 0; i < 10; i++)

delete ans[i]; // each insts deleted

delete[] ans; // ar deleted-ed

show execution

talk about random number generation

Multiple Inheritance

So far, we’ve only talked about single inheritance, in which each derived class has a unique parent. With single inheritance, a class may have many ancestors, but it extends online one of them. C++ also supports multiple inheritance, however, in which a class may have two or more parents.

What is MCs?

A canonical example is voicemail. A voicemail system has some of the features of a telephone and some of the features of email. In a language with only single inheritance, we could plausibly choose either class as the parent (or, as a compromise, we might use composition to place an instance of each inside the voicemail object). With multiple inheritance, though, the voicemail class can extend both.

What if the same data member, or member function, is inherited from two ancestors? Well, that’s one of the complications…

Parent constructor calls should be explicit?

Let’s begin with a very simple, even trivial example. In chapter ???, we used a Point class, representing a point in the Cartesian plane. The class contained a two doubles, x and y. In this example, we’re going to re-create the Point class, as an extension of two classes, one for each coordinate, in order to study the mechanics of multiple inheritance. Here is XCoord:

class XCoord {

private:

double x;

public:

XCoord(double val): x(val) {

cout < "In XCoord CONStructor.\n";

}

virtual ~XCoord() {

cout < "In XCoord DEStructor.\n";

}

};

YCoord is similar. We can now define our new Point class:

class Point: public XCoord, public YCoord {

public:

Point(double v1, double v2):

XCoord(v1), YCoord(v2) {

cout < "In Point CONStructor.\n";

}

~Point() {

cout < "In Point DEStructor.\n";

}

};

When we run a program containing just the line:

Point pt(5,10);

we obtain the following output:

In XCoord CONStructor.

In YCoord CONStructor.

In Point CONStructor.

In Point DEStructor.

In YCoord DEStructor.

In XCoord DEStructor.

The first three lines are the output resulting from the construction of pt.We enter Point’s constructor, but first, before executing any of the code there, we call the constructors of Point’s two parents.(in the order Point lists them as parents). After this, we execute the body of Point’s constructor. In constructing, we construct from the bottom up.

The last three lines come from the destruction of pt that occurs at the end of the main function. Following the intuition that Point is built on top of XCoord and YCoord, we destroy Point before destroying them. Notice that in destruction, we handle YCoord before XCoord. We destroy in reverse order of construction (conform always true).

Multiple inheritance & dynamic casting

When we cast a pointer, we get back a pointer to some part of the original entity (confirm). Usually, base classs info is stored at the beginning of the derived-class instances, so casting the instance evaluates to a pointer to the base-class instance. The cast pointer and the original pointer are equal (but are of different types). (system dependencies)

This can’t be guaranteed in the case of multiple inheritance, because we have multiple parent classes. They can’t both be at the front. Consider the following code:

Point* pp = new Point(5,10);

XCoord* xp = pp;

YCoord* yp = pp;

cout < "pp address == " < reinterpret_cast<void*>(pp) < endl;

cout < "xp address == " < reinterpret_cast<void*>(xp) < endl;

cout < "yp address == " < reinterpret_cast<void*>(yp) < endl;

delete pp;

The output is:

pp address == 00853200

xp address == 00853200

yp address == 00853210

Explain…

Varieties of casting

Old-fashioned, all-purpose casting, still used in C and Java, takes the form of the () operator:

x = (int)2.5;

As noted before, this type is deprecated. (Did we talk about reasons? Conflates different operations in one syntax; hard to search for.) There are four sanctioned types, that play four different roles:

  • static_cast
  • reinterpret_cast
  • dynamic_cast
  • const_cast

Static casting is probably the first thing that comes to mind when we think of casting. Applied to a value of the source type, a static cast evaluates to (an approximation of) the corresponding or analogous value of the destination type. The new value is often close but different. (Usually limited to primitives?) The following cast is the new-style equivalent of the old-fashioned cast above:

x = static_cast<int>(2.5)

Reinterpret casting is usually used when we need to treat one pointer type as another. (applies to non-pointers?). One time we used this was when we wanted to print the address of a char*, rather than the characters themselves. The cast itself looks like this:

p = reinterpret_cast<void*>(s);

Dynamic casting is the type we just looked at, in which we cast a pointer up and down the class hierarchy. The pointer refers to a single object of one, fixed type, but the pointer variable may be declared to point to an ancestor of the actual object type. By casting down, we get back a pointer to the derived type, which gives us access to its members. Not only does the cast have no effect on the underlying object, it doesn’t even cast the underlying object. All it casts is the pointer referring to that object, with the result that a dereferencing of that pointer will interpret the object as an instance of the requested type. We saw that a dynamic cast takes the following from:

ep = dynamic_cast Extended*>(bp);

Finally, const casting toggles the mutability of a variable off or on. For example,

const double PI = 3.14;

const double* cp = &PI;

double* p = const_cast<double *>(cp);

*p = 4;

cout < "PI == " < PI < endl;

The surprising, subversive result is:

PI == 4

Even though PI was declared as a constant, we were able to cast the referent of cp as a plain, mutable double*. (did we need the pointer at all?) By casting, we can prevent warnings or errors and modify something we’ve declared as a const. (But that doesn’t mean we should!)

Overloading and inheritance

In general, for declared derived-class instances, we call the “lowest” implemented version of the function named. So if ftn() is defined in both Base and Derived, Derived::ftn() hides the Base version and is the one called. Unfortunately, however, Defrived::ftn() hides any function in Base with the name ftn, irrespective of arguments. In this case, overloading is not automatically inherited, although the overloaded function can be redeclared in Derived.

Flesh out with an example

Access levels and inheritance

Initially, we did public inheritance:

class Derived: public Base { … }

in which public and protected members of Base became public and protected members, respectively of Derived, but private members of Base were inaccessible to Derived. Also, in the case of what?, we have private inheritance:

class Derived: private Base { … }

With private inheritance, private members of Base are again inaccessible to Derived, but both public and protected members of Base become private members of Derived. The result is that both kinds of members are accessible inside Derived but not outside of it or in its descendents.

Finally, there is protected inheritance:

class Derived: protected Base { … }

Private members of Base are once again inaccessible to Derived. Public and private members of Base will not be accessible outside of Derived, but they are accessible both inside Derived and, potentially, inside its descendents.

The following table summaries the access levels of members in the derived class:

Public inheritance / Protected inheritance / Private inheritance
Public member / Public in derived / Protected in derived / Private in derived
Protected member / Protected in derived / Protected in derived / Private in derived
Private member / Hidden in derived / Hidden in derived / Hidden in derived

Virtual destructors

If Base and Derived have constructors and destructors, when are they called? (didn’t we already do this?) First, consider no-param constructors (inhercd.cpp?):