|














Am avut 371689 vizite de la lansarea siteului.

|
|
Inapoi
Inainte
Cuprins
Operator
overloading
You can make operators virtual
just like other member functions. Implementing virtual operators often
becomes confusing, however, because you may be operating on two objects, both
with unknown types. This is usually the case with mathematical components (for
which you often overload operators). For example, consider a system that deals
with matrices, vectors and scalar values, all three of which are derived from
class Math:
//: C15:OperatorPolymorphism.cpp
// Polymorphism with overloaded operators
#include <iostream>
using namespace std;
class Matrix;
class Scalar;
class Vector;
class Math {
public:
virtual Math& operator*(Math& rv) = 0;
virtual Math& multiply(Matrix*) = 0;
virtual Math& multiply(Scalar*) = 0;
virtual Math& multiply(Vector*) = 0;
virtual ~Math() {}
};
class Matrix : public Math {
public:
Math& operator*(Math& rv) {
return rv.multiply(this); // 2nd dispatch
}
Math& multiply(Matrix*) {
cout << "Matrix * Matrix" << endl;
return *this;
}
Math& multiply(Scalar*) {
cout << "Scalar * Matrix" << endl;
return *this;
}
Math& multiply(Vector*) {
cout << "Vector * Matrix" << endl;
return *this;
}
};
class Scalar : public Math {
public:
Math& operator*(Math& rv) {
return rv.multiply(this); // 2nd dispatch
}
Math& multiply(Matrix*) {
cout << "Matrix * Scalar" << endl;
return *this;
}
Math& multiply(Scalar*) {
cout << "Scalar * Scalar" << endl;
return *this;
}
Math& multiply(Vector*) {
cout << "Vector * Scalar" << endl;
return *this;
}
};
class Vector : public Math {
public:
Math& operator*(Math& rv) {
return rv.multiply(this); // 2nd dispatch
}
Math& multiply(Matrix*) {
cout << "Matrix * Vector" << endl;
return *this;
}
Math& multiply(Scalar*) {
cout << "Scalar * Vector" << endl;
return *this;
}
Math& multiply(Vector*) {
cout << "Vector * Vector" << endl;
return *this;
}
};
int main() {
Matrix m; Vector v; Scalar s;
Math* math[] = { &m, &v, &s };
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++) {
Math& m1 = *math[i];
Math& m2 = *math[j];
m1 * m2;
}
} ///:~
For simplicity, only the operator*
has been overloaded. The goal is to be able to multiply any two Math
objects and produce the desired result – and note that multiplying a
matrix by a vector is a very different operation than multiplying a vector by a
matrix.
The problem is that, in
main( ), the expression m1 * m2 contains two upcast
Math references, and thus two objects of unknown type. A virtual function
is only capable of making a single dispatch – that is, determining the
type of one unknown object. To determine both types a technique called
multiple dispatching is used in this example, whereby what appears to be
a single virtual function call results in a second virtual call. By the time
this second call is made, you’ve determined both types of object, and can
perform the proper activity. It’s not transparent at first, but if you
stare at the example for awhile it should begin to make sense. This topic is
explored in more depth in the Design Patterns chapter in Volume 2, which you can
download at www.BruceEckel.com.
Downcasting
As you might guess, since there’s
such a thing as upcasting – moving up an
inheritance hierarchy – there should also be
downcasting to move down
a hierarchy. But upcasting is easy since as you move up an inheritance hierarchy
the classes always converge to more general classes. That is, when you upcast
you are always clearly derived from an ancestor class (typically only one,
except in the case of multiple inheritance) but when you downcast there are
usually several possibilities that you could cast to. More specifically, a
Circle is a type of Shape (that’s the upcast), but if you
try to downcast a Shape it could be a Circle, Square,
Triangle, etc. So the dilemma is figuring out a way to safely downcast.
(But an even more important issue is asking yourself why you’re
downcasting in the first place instead of just using polymorphism to
automatically figure out the correct type. The avoidance of downcasting is
covered in Volume 2 of this book.)
C++ provides a special
explicit cast (introduced
in Chapter 3) called
dynamic_cast that is a
type-safe downcast operation. When you use
dynamic_cast to try to cast down to a particular type, the return value
will be a pointer to the desired type only if the cast is proper and successful,
otherwise it will return zero to indicate that this was not the correct type.
Here’s a minimal example:
//: C15:DynamicCast.cpp
#include <iostream>
using namespace std;
class Pet { public: virtual ~Pet(){}};
class Dog : public Pet {};
class Cat : public Pet {};
int main() {
Pet* b = new Cat; // Upcast
// Try to cast it to Dog*:
Dog* d1 = dynamic_cast<Dog*>(b);
// Try to cast it to Cat*:
Cat* d2 = dynamic_cast<Cat*>(b);
cout << "d1 = " << (long)d1 << endl;
cout << "d2 = " << (long)d2 << endl;
} ///:~
When you use dynamic_cast, you
must be working with a true polymorphic hierarchy – one with
virtual functions – because dynamic_cast
uses information stored in the VTABLE to determine the actual type. Here, the
base class contains a virtual destructor and that suffices. In
main( ), a Cat pointer is upcast to a Pet, and then a
downcast is attempted to both a Dog pointer and a Cat pointer.
Both pointers are printed, and you’ll see when you run the program that
the incorrect downcast produces a zero result. Of course, whenever you downcast
you are responsible for checking to make sure that the result of the cast is
nonzero. Also, you should not assume that the pointer will be exactly the same,
because sometimes pointer adjustments take place during upcasting and
downcasting (in particular, with multiple inheritance).
A dynamic_cast requires a little
bit of extra overhead to run; not much, but if you’re doing a lot of
dynamic_casting (in which case you should be seriously questioning your
program design) this may become a performance issue. In some cases you may know
something special during downcasting that allows you to say for sure what type
you’re dealing with, in which case the extra overhead of the
dynamic_cast becomes unnecessary, and you can use a
static_cast instead.
Here’s how it might work:
//: C15:StaticHierarchyNavigation.cpp
// Navigating class hierarchies with static_cast
#include <iostream>
#include <typeinfo>
using namespace std;
class Shape { public: virtual ~Shape() {}; };
class Circle : public Shape {};
class Square : public Shape {};
class Other {};
int main() {
Circle c;
Shape* s = &c; // Upcast: normal and OK
// More explicit but unnecessary:
s = static_cast<Shape*>(&c);
// (Since upcasting is such a safe and common
// operation, the cast becomes cluttering)
Circle* cp = 0;
Square* sp = 0;
// Static Navigation of class hierarchies
// requires extra type information:
if(typeid(s) == typeid(cp)) // C++ RTTI
cp = static_cast<Circle*>(s);
if(typeid(s) == typeid(sp))
sp = static_cast<Square*>(s);
if(cp != 0)
cout << "It's a circle!" << endl;
if(sp != 0)
cout << "It's a square!" << endl;
// Static navigation is ONLY an efficiency hack;
// dynamic_cast is always safer. However:
// Other* op = static_cast<Other*>(s);
// Conveniently gives an error message, while
Other* op2 = (Other*)s;
// does not
} ///:~
In this program, a new feature is used
that is not fully described until Volume 2 of this book, where a chapter is
given to the topic: C++’s
run-time
type information (RTTI) mechanism. RTTI allows you to discover type
information that has been lost by upcasting. The dynamic_cast is actually
one form of RTTI. Here, the
typeid keyword (declared
in the header file
<typeinfo>)
is used to detect the types of the pointers. You can see that the type of
the upcast Shape pointer is successively compared to a Circle
pointer and a Square pointer to see if there’s a match.
There’s more to RTTI than typeid, and you can also imagine that it
would be fairly easy to implement your own type information system using a
virtual function.
A Circle object is created and the
address is upcast to a Shape pointer; the second version of the
expression shows how you can use static_cast to be more explicit about
the upcast. However, since an upcast is always safe and it’s a common
thing to do, I consider an
explicit
cast for upcasting to be cluttering and unnecessary.
RTTI is used to determine the type, and
then static_cast is used to perform the downcast. But notice that in this
design the process is effectively the same as using dynamic_cast, and the
client programmer must do some testing to discover the cast that was actually
successful. You’ll typically want a situation that’s more
deterministic than in the example above before using static_cast rather
than dynamic_cast (and, again, you want to carefully examine your design
before using dynamic_cast).
If a class hierarchy has no
virtual functions (which is a questionable design) or if you have other
information that allows you to safely downcast, it’s a tiny bit faster to
do the downcast statically than
with dynamic_cast. In addition, static_cast won’t allow you
to cast out of the hierarchy, as the traditional cast will, so it’s safer.
However, statically navigating class hierarchies is always risky and you should
use dynamic_cast unless you have a special
situation.
Summary
Polymorphism
– implemented in C++ with virtual functions
– means “different forms.” In object-oriented programming, you
have the same face (the common interface in the base class) and different forms
using that face: the different versions of the virtual
functions.
You’ve seen in this chapter that
it’s impossible to understand, or even create, an example of polymorphism
without using data abstraction and inheritance. Polymorphism is a feature that
cannot be viewed in isolation (like const or a switch statement,
for example), but instead works only in concert, as part of a “big
picture” of class relationships. People are often confused by other,
non-object-oriented features of C++, like overloading and default arguments,
which are sometimes presented as object-oriented. Don’t be fooled; if it
isn’t late binding, it isn’t polymorphism.
To use polymorphism – and thus,
object-oriented techniques – effectively in your programs you must expand
your view of programming to include not just members and messages of an
individual class, but also the commonality among classes and their relationships
with each other. Although this requires significant effort, it’s a worthy
struggle, because the results are faster program development, better code
organization, extensible programs, and easier code maintenance.
Polymorphism completes the
object-oriented features of the language, but there are two more major
features in C++: templates (which are introduced in
Chapter 16 and covered in much more detail in Volume 2), and exception handling
(which is covered in Volume 2). These features provide you as much increase in
programming power as each of the object-oriented features: abstract data typing,
inheritance, and
polymorphism.
Exercises
Solutions to selected exercises
can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from
www.BruceEckel.com.
- Create a simple
“shape” hierarchy: a base class called Shape and derived
classes called Circle, Square, and Triangle. In the base
class, make a virtual function called draw( ), and override this in
the derived classes. Make an array of pointers to Shape objects that you
create on the heap (and thus perform upcasting of the pointers), and call
draw( ) through the base-class pointers, to verify the behavior of
the virtual function. If your debugger supports it, single-step through the
code.
- Modify
Exercise 1 so draw( ) is a pure virtual function. Try creating an
object of type Shape. Try to call the pure virtual function inside
the constructor and see what happens. Leaving it as a pure virtual, give
draw( ) a
definition.
- Expanding
on Exercise 2, create a function that takes a Shape object by
value and try to upcast a derived object in as an argument. See what
happens. Fix the function by taking a reference to the Shape
object.
- Modify
C14:Combined.cpp so that f( ) is virtual in the base
class. Change main( ) to perform an upcast and a virtual
call.
- Modify
Instrument3.cpp by adding a virtual prepare( ) function.
Call prepare( ) inside
tune( ).
- Create
an inheritance hierarchy of Rodent: Mouse, Gerbil,
Hamster, etc. In the base class, provide methods that are common to all
Rodents, and redefine these in the derived classes to perform different
behaviors depending on the specific type of Rodent. Create an array of
pointers to Rodent, fill it with different specific types of
Rodents, and call your base-class methods to see what
happens.
- Modify
Exercise 6 so that you use a vector<Rodent*> instead of an array of
pointers. Make sure that memory is cleaned up
properly.
- Starting
with the previous Rodent hierarchy, inherit BlueHamster from
Hamster (yes, there is such a thing; I had one when I was a kid),
override the base-class methods, and show that the code that calls the
base-class methods doesn’t need to change in order to accommodate the new
type.
- Starting with
the previous Rodent hierarchy, add a non virtual destructor, create an
object of class Hamster using new, upcast the pointer to a
Rodent*, and delete the pointer to show that it doesn’t call
all the destructors in the hierarchy. Change the destructor to be virtual
and demonstrate that the behavior is now
correct.
- Starting
with the previous Rodent hierarchy, modify Rodent so it is a pure
abstract base
class.
- Create an
air-traffic control system with base-class Aircraft and various derived
types. Create a Tower class with a vector<Aircraft*> that
sends the appropriate messages to the various aircraft under its
control.
- Create a
model of a greenhouse by inheriting various types of Plant and building
mechanisms into your greenhouse that take care of the
plants.
- In
Early.cpp, make Pet a pure abstract base
class.
- In
AddingVirtuals.cpp, make all the member functions of Pet pure
virtuals, but provide a definition for name( ). Fix Dog as
necessary, using the base-class definition of
name( ).
- Write
a small program to show the difference between calling a virtual function inside
a normal member function and calling a virtual function inside a constructor.
The program should prove that the two calls produce different
results.
- Modify
VirtualsInDestructors.cpp by inheriting a class from Derived and
overriding f( ) and the destructor. In main( ), create
and upcast an object of your new type, then delete
it.
- Take Exercise 16
and add calls to f( ) in each destructor. Explain what
happens.
- Create a
class that has a data member and a derived class that adds another data member.
Write a non-member function that takes an object of the base class by
value and prints out the size of that object using sizeof. In
main( ) create an object of the derived class, print out its size,
and then call your function. Explain what
happens.
- Create a
simple example of a virtual function call and generate assembly output. Locate
the assembly code for the virtual call and trace and explain the
code.
- Write a class
with one virtual function and one non-virtual function. Inherit a new class,
make an object of this class, and upcast to a pointer of the base-class type.
Use the clock( ) function found in <ctime>
(you’ll need to look this up in your local C library guide) to measure the
difference between a virtual call and non-virtual call. You’ll need to
make multiple calls to each function inside your timing loop in order to see the
difference.
- Modify
C14:Order.cpp by adding a virtual function in the base class of the
CLASS macro (have it print something) and by making the destructor
virtual. Make objects of the various subclasses and upcast them to the base
class. Verify that the virtual behavior works and that proper construction and
destruction takes
place.
- Write a class
with three overloaded virtual functions. Inherit a new class from this and
override one of the functions. Create an object of your derived class. Can you
call all the base class functions through the derived-class object? Upcast the
address of the object to the base. Can you call all three functions through the
base? Remove the overridden definition in the derived class. Now can you call
all the base class functions through the derived-class
object?
- Modify
VariantReturn.cpp to show that its behavior works with references as well
as pointers.
- In
Early.cpp, how can you tell whether the compiler makes the call using
early or late binding? Determine the case for your own
compiler.
- Create
a base class containing a clone( ) function that returns a pointer
to a copy of the current object. Derive two subclasses that override
clone( ) to return copies of their specific types. In
main( ), create and upcast objects of your two derived types, then
call clone( ) for each and verify that the cloned copies are the
correct subtypes. Experiment with your clone( ) function so that you
return the base type, then try returning the exact derived type. Can you think
of situations in which the latter approach is
necessary?
- Modify
OStackTest.cpp by creating your own class, then multiply-inheriting it
with Object to create something that can be placed into the Stack.
Test your class in
main( ).
- Add
a type called Tensor to
OperatorPolymorphism.cpp.
- (Intermediate)
Create a base class X with no data members and no constructor, but with a
virtual function. Create a class Y that inherits from X, but
without an explicit constructor. Generate assembly code and examine it to
determine if a constructor is created and called for X, and if so, what
the code does. Explain what you discover. X has no default constructor,
so why doesn’t the compiler
complain?
- (Intermediate)
Modify Exercise 28 by writing constructors for both classes so that each
constructor calls a virtual function. Generate assembly code. Determine where
the VPTR is being assigned inside each constructor. Is the virtual mechanism
being used by your compiler inside the constructor? Establish why the local
version of the function is still being
called.
- (Advanced)
If function calls to an object passed by value weren’t early-bound,
a virtual call might access parts that didn’t exist. Is this possible?
Write some code to force a virtual call, and see if this causes a crash. To
explain the behavior, examine what happens when you pass an object by
value.
- (Advanced)
Find out exactly how much more time is required for a virtual function call by
going to your processor’s assembly-language information or other technical
manual and finding out the number of clock states required for a simple call
versus the number required for the virtual function
instructions.
- Determine
the sizeof the VPTR for your implementation. Now multiply-inherit two
classes that contain virtual functions. Did you get one VPTR or two in the
derived
class?
- Create a
class with data members and virtual functions. Write a function that looks at
the memory in an object of your class and prints out the various pieces of it.
To do this you will need to experiment and iteratively discover where the VPTR
is located in the
object.
- Pretend that
virtual functions don’t exist, and modify Instrument4.cpp so that
it uses dynamic_cast to make the equivalent of the virtual calls. Explain
why this is a bad
idea.
- Modify
StaticHierarchyNavigation.cpp so that instead of using C++ RTTI you
create your own RTTI via a virtual function in the base class called
whatAmI( ) and an enum type { Circles, Squares
};.
- Start with
PointerToMemberOperator.cpp from Chapter 12 and show that polymorphism
still works with pointers-to-members, even if operator->* is
overloaded.
[54]
Compilers may implement virtual behavior any way they want, but the way
it’s described here is an almost universal approach.
[55]
Some compilers might have size issues here but it will be rare.
[56]
Smalltalk, Java, and Python, for instance, use this approach with great
success.
[57]
At Bell Labs, where C++ was invented, there are a lot of C programmers.
Making them all more efficient, even just a bit, saves the company many
millions.
[58]
Actually, not all pointers are the same size on all machines. In the context of
this discussion, however, they can be considered to be the
same.
 |
| |
|