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.

  1. 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.
  2. 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.
  3. 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.
  4. Modify C14:Combined.cpp so that f( ) is virtual in the base class. Change main( ) to perform an upcast and a virtual call.
  5. Modify Instrument3.cpp by adding a virtual prepare( ) function. Call prepare( ) inside tune( ).
  6. 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.
  7. Modify Exercise 6 so that you use a vector<Rodent*> instead of an array of pointers. Make sure that memory is cleaned up properly.
  8. 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.
  9. 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.
  10. Starting with the previous Rodent hierarchy, modify Rodent so it is a pure abstract base class.
  11. 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.
  12. Create a model of a greenhouse by inheriting various types of Plant and building mechanisms into your greenhouse that take care of the plants.
  13. In Early.cpp, make Pet a pure abstract base class.
  14. 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( ).
  15. 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.
  16. 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.
  17. Take Exercise 16 and add calls to f( ) in each destructor. Explain what happens.
  18. 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.
  19. 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.
  20. 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.
  21. 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.
  22. 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?
  23. Modify VariantReturn.cpp to show that its behavior works with references as well as pointers.
  24. 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.
  25. 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?
  26. 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( ).
  27. Add a type called Tensor to OperatorPolymorphism.cpp.
  28. (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?
  29. (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.
  30. (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.
  31. (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.
  32. 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?
  33. 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.
  34. 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.
  35. 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 };.
  36. 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.

Home   |   Web Faq   |   Radio Online   |   About   |   Products   |   Webmaster Login

The quality software developer.™
© 2003-2004 ruben|labs corp. All Rights Reserved.
Timp de generare a paginii: 17583 secunde
Versiune site: 1.8 SP3 (build 2305-rtm.88542-10.2004)