CS320 Lecture:  Inheritance in C++; Inherited and               9/20/96
                 Overridden Methods; Protected Members;         revised 9/24/97
                Polymorphism in C++: Static vs Dynamic
                 Binding; Virtual Methods; Pure
                 Virtual Methods and Abstract Classes
                Operator Overloading

Materials:      Transparencies: transaction.h - Transaction,WithdrawlTransaction
                                transaction.cc - chooseTransaction +
                                                 WithdrawlTransaction::
                                session.h

                                Piece class hierarchy and testers
                                Ditto with virtual on Piece::kind()
                                Ditto with Piece::kind() pure virtual
                                Stroustrup Design/Evolution p. 75

                                money.h
                                money.cc
                                excerpts from bank.cc - operators on Money

                                C++ Reference Manual 13.4

                Executables:    POLY1.EXE
                                POLY2.EXE

I. Inheritance in C++
-  ----------- -- ---

   A. At the start of the course, we said that the distinctive features of OO
      could be remembered by the acronym "PIE".

      1. What does this stand for?

         ASK

      2. So far, we have talked about how encapsulation is implemented in C++
         by using classes with public and private members.

      3. We now turn to inheritance and polymorphism

   B. The basic C++ syntax for implementing inheritance is simple: a class
      explicitly names the base class (if any) of the class being declared

      TRANSPARENCY - transaction.h

      declaration of WithdrawlTransaction

      1. Note the colon between the class name and the base class
         specifier

      2. Note the use of the reserved word "public".

         a. This specifies that all the public members of the base class 
            become public members of the derived class.  (Private members 
            of the base class remain private, of course).

         b. It is also possible to use the words "protected" and "private"
            here.  For example, if private were used, all the members of the
            base class would become private members of the derived class.

         c. The key idea is that this can be used to REDUCE the accessibility
            of members, but NOT to increase it.

         d. In practice, one rarely sees anything but "public" here.

   C. In C++, it is possible for a class to have more than one base class.
      This is called MULTIPLE INHERITANCE, and is specified by listing the
      base classes separated by commas.

      1. Example:

         class foo : public bar, public baz

                ...

      2. We save further discussion of multiple inheritance for later in the
         course.

   D. When a C++ class is derived from another class, the following happens

      1. It inherits all of the instance variables, to which it can add
         instance variables of its own.

         Example - TRANSPARENCY - transaction.h

                   declarations of Transaction, WithdrawlTransaction

                   NOTE: COMMENTS HAVE BEEN EDITED OUT TO FIT ON ONE PAGE

         Each WithdrawlTransaction object has the following fields:

         Inherited from Transaction:

         _session
         _atm
         _bank
         _serialNumber
         _newBalance
         _availableBalance
         (_lastSerialNumberAssigned is a class variable shared by all
          intances of Transaction and its subclasses)

         Added by WithdrawlTransaction:

         _fromAccount
         _amount

      2. It responds to all of the messages its base class responds to,
         to which it can add messages of its own.  In the case of messages
         inherited from the base class, it can either use the base class's
         methods or OVERRIDE the base class method with one of its own.

         Example - TRANSPARENCY - transaction.h

                   declarations of Transaction, WithdrawlTransaction

         A WithdrawlTransaction object handles the following messages:

         doTransactionUseCase                   - using inherited method
         getTransactionSpecificsFromCustomer    - overridden
         sendToBank                             - overridden
         finishApprovedTransaction              - overridden

         a. Note that overridden methods are explicitly declared in the
            interface of the derived class, while inherited methods are
            not.

         b. In this case, WithdrawlTransaction does not add any additional
            messages to those inherited from Transaction.

         c. Note: the static method chooseTransaction is a class method and so 
            is not associated with a specific object

      3. Constructors and destructors are handled specially.

         a. The constructor method for a derived class automatically calls
            the constructor method for its base class BEFORE its own code
            is executed.  The code to do this is generated by the compiler.

            If the base class constructor requires parameters, they must be
            provided by listing the base class constructor with its parameters
            in the initializer list of the constructor.

            Example: TRANSPARENCY: transaction.cc
                
                     WithdrawlTransaction::WithdrawlTransaction()

            i. Note how the session, atm, and bank parameters to 
               WithdrawlTransaction() are passed on to the Transaction() 
               constructor

           ii. In addition to copying these into the _session, _atm, and _bank
               fields, the Transaction constructor will also initialize 
               _serialNumber

         b. In like manner, the destructor method for a derived class
            automatically calls the destructor method for its base class
            AFTER its own code is executed.  Nothing explicit need be
            supplied, since destructors never take parameters.

         c. Note the order - construction occurs in the order base class,
            derived class; destruction in the reverse order.  This is always
            the case in C++ - destructors are called in the reverse order of
            the corresponding constructors (LIFO).

   E. In our discussion of encapsulation, we mentioned that class members can
      have either public or private access.  When we use inheritance, a third 
      access becomes relevant: protected.

      1. As your recall, a private member is accessible only from the methods
         of the class in which it is declared, and a public member is
         accessible everywhere.

      2. A protected member is accessible only to the methods of the class in
         which it is declared AND ITS SUBCLASSES.

         Example: TRANSPARENCY - transaction.h

         a. The data members _session, _atm, _bank, _serialNumber, _balance, and
            _newBalance are accessible both to the methods of Transaction and 
            to the methods of WithdrawlTransaction (and other subclasses).

         b. The data member _lastSerialNumberAssigned is accessible only to the
            methods of Transaction.  This is what we want, because the only
            time it is used is as part of the process of initializing
            _serialNumber in the Transaction constructor.

           (Note: the protection on this item and the fact that it is static
            are totally independent issues).

II. Polymorphism in C++
--  ------------ -- ---

   A. The final piece of the OO "PIE" we need to deal with is Polymorphism.

      1. The essential idea behind polymorphism is that different objects
         can respond to the same message by using different methods.

      2. Actually, as we shall see, polymorphism takes a variety of shapes.
         (No pun intended :-) ).

   B. One form of polymorphism is based on the fact that whenever a method is
      invoked it is always associated with some specific class; thus, two
      or more classes can have methods with the same nate without causing
      any confusion.

      1. Example: In our ATM example, the class both ATM and CardReader have
         methods called:

         ejectCard()
         retainCard()

         and both ATM and CashDispenser have a method called

         dispenseCash()

         and both ATM and EnvelopeAcceptor have a method called

         acceptEnvelope()

         The ATM version of these methods is implemented by calling the
         corresponding method of one of its components, perhaps in conjunction
         with displaying some message on the Display.
        
      2. Methods with names like "get", "put", "insert", "remove", "lookup"
         etc. tend to occur in many classes.

      3. This kind of polymorphism is handled by the compiler, as follows:

         a. Each method invocation is associated with a specific class by
            virtue of either being applied to a specific object or by the
            explicit use of CLASSNAME::

         b. The compiler determines the correct method to use by looking up
            the name in its table of methods for the appropriate class.

   C. Another form of polymorphism is based on the types of the parameters to 
      a function or method.

      1. In Pascal, we got used to the idea that a given identifier could
         have only one declaration in any given context.

      2. In C++ - as in many other languages - an identifier can name more than 
         one function or method, provided that each has a distinguishable 
         parameter list.

         a. Example: We might define two versions of a function to
            calculate a square root - e.g.

            int sqrt(int x);
            float sqrt(float x);

            i. If we invoke sqrt(2), we get the int version (which returns 1)

           ii. If we invoke sqrt(2.0), we get the float version (which returns
               1.414...

         b. Example: In our chess game example, we might have three different
            constructors for class Game:

            Game();                       // Initiate a game - challenge someone
            Game(const char * challenger);// Respond to a challenge from someone
            Game(GameFile & file);        // Resume a saved game

         c. Example: In a game playing program where a human plays against
            the machine (rather than another human), we might want a player
            who has made a mistake to restart the game at some earlier point,
            or even at the beginning.  We might have two restart methods:

            void restart();             // Start over from beginning
            void restart(int noMoves);  // Backup a specified number of moves

      3. C++ allows any number of functions or methods in a class to have the
         same name, as long as each has a distinct SIGNATURE.

         a. The signature of a function is constructed from the types of its 
            parameters - e.g. the signature of

            int foo(int x, float y)

            is (int, float)

         b. The signature of a method is constructed in the same way, 
            including the implicit first parameter - e.g. the signature of a
            method of class Game declared as

            void restart(int noMoves)

            is (Game *, int)

         c. Note that the return value type - if any - is NOT a part of the
            signature.  That is, one could not declare two functions or
            methods with the same name and parameter types, differing only
            in the type of the return value - so:

                int foo(int x);
                float foo(int x);

            would NOT be allowed.

      4. We say that an identifier that names two or more different functions
         or methods in this way is OVERLOADED.

      5. Overloading is handled by the compiler as follows:

         a. When a function or method is called, the compiler determines the
            signature of the needed method from the actual parameters used.

         b. It then chooses the correct version of an overloaded function or 
            method by matching the signature of the method and the call.

         c. Note that if there is no method whose signature exactly matches the
            call, the compiler applies certain automatic type conversions and
            tries again to find a match.

            Example: suppose we had methods declared 

                        foo(int i) ...
                        foo(float f) ...
                        foo(double d) ...

                     and the calls:

                        foo(1);
                        foo(1.5);
                        foo(6.02d23);

                     These would generate calls to the integer, float, and
                     double methods, respectively.

                     However, if we had only the double method, all three
                     would use it, because the compiler automatically converts
                     an int to a double if necessary to satisfy the declared
                     argument type of a function.

                     (NOTE: the full set of rules for such conversions is
                      complex and can lead to ambiguities in some cases)
 
   D. Another form of polymorphism arises when inheritance is used.

      1. Recall that the relationship between a derived class (subclass) and its
         base class is called "is a".  Any object that is an instance of a 
         derived class is also, in a very real sense, an instance of the base 
         class(es) of from which the subclass is derived.

         a. It has all the instance variables of the base class (plus, perhaps
            more).

         b. It responds to all the messages the base class responds to (plus,
            perhaps more) - though it may do so in a different way.

      2. In C++ and other OO languages, a consequence of this fact is that an
         object of a derived class can often be used where an object of the
         base class is expected.

         a. Example: TRANSPARENCY - transaction.cc

            method chooseTransaction is declared to return a value of type
            Transaction * - i.e. a pointer to a Transaction.  But what it
            actually returns is a pointer to a particular subclass of
            Transaction: WithdrawlTransaction or DepositTransaction or ...

         b. Example: TRANSPARENCY - session.h

            field _currentTransaction is declared to be a pointer to a
            Transaction - but what it will actually contain is a pointer to
            a WithdrawlTransaction or DepositTransaction or ...

         c. In particular, the following general rule holds: a variable that
            is declared to hold a POINTER or a REFERENCE to an object of
            some class can always receive as its value a pointer or reference
            (as the case may be) to an object of ANY SUBCLASS of the declared 
            type (as well, of course, as the class itself).

            Example: a variable declared Transaction & or Transaction * can
                     receive as its value a reference / pointer (as the case
                     may be) to a Transaction object, a WithdrawlTransaction
                     object, a DepositTransaction object ...

         d. Note, though, that a variable declared to be of a class type (not
            pointer or reference to a class type) can only be assigned an
            object of its exact class

            Example: If we have the declaration

                        Transaction t;

                     We CANNOT store a WithdrawlTransaction into t.

            (The reason for this is that, in general, a subclass object can
             be bigger than an object of its base class due to added
             fields).

      3. Thus, we can have polymorphic references and pointers in C++, which
         in turn allows us to create POLYMORPHIC CONTAINERS (classes that can
         contain objects of more than one type)

         a. Example: Consider implementing a chess game.  We have six basic 
            types of piece (King, Queen, Bishop, Knight, Rook and Pawn) each
            of which can be represented by a subclass of a class Piece.

            The chess board is an 8 x 8 array of squares, each of which can
            hold any type of piece.  We might, then, see something like this:

            - In the declaration of class Board:

                Piece * _square[8][8];

            - In the implementation of the constructor that sets the
              board to its initial state:

                _square[0][0] = new Rook(WHITE);
                _square[0][1] = new Knight(WHITE);
                _square[0][2] = new Bishop(WHITE);
                ...

          b. In so doing, of course, we lose some information about the
             specific type of each object in the container.

             Example: Our chess board might have a method getPiece() which
             returns the piece in a specific slot on the board (or NULL if
             it is vacant).  The return type of this method must be Piece *,
             which means that the caller of the method cannot directly know 
             what type of Piece is coming back, and can only invoke the methods
             that are common to all Pieces on the result.

          c. One approach to solving this problem might be to have a method
             called kind() defined for class Piece that allows a Piece to tell
             its caller what kind of Piece.

             i. TRANSPARENCY - Piece class hierarchy

            ii. Now suppose we wrote the following program:

                TRANSPARENCY - Piece class tester #1

                Not surprisingly, the output of this program is

                ? King Queen

           iii. But now, continuing the above example, suppose we wrote the
                following instead:

                TRANSPARENCY - Piece class tester #2

                If the user types K, what will get printed?  What about Q?

                ASK

                DEMO POLY1.EXE

                - Recall that, in C++, the selection of the correct method to
                  use in a case like this is made by the COMPILER, and thus
                  must be based on information available at COMPILE TIME.

                - As a result, the version of kind() that will be selected
                  will be based on the type of the variable ptr, which is
                  Piece *.  Thus, the method Piece::Kind will always be
                  selected, and the output will always be "?".

                - This approach to choosing the correct method to use in a
                  case like this is called STATIC (or EARLY) BINDING.

            iv. As this example illustrates, the method we have been trying
                will NOT allow us to recover type information from a
                polymorphic pointer.

          d. Overcoming this problem involves another form of polymorphism.

   E. We have looked at a number of forms of polymorphism which depend on the
      COMPILER choosing the correct meaning for a polymorphic name, based on
      how it is used.  This choice must, of necessity, be made at COMPILE
      TIME, based on information available then.  We now turn to a form of
      polymorphism that defers the interpretation of a polymorphic name
      to RUN-TIME.

      1. TRANSPARENCY - Revised Piece hierarchy with virtual on kind() in
         Piece

         a. Only one difference - ASK

         b. "virtual" on first declaration of kind()

      2. DEMO - POLY2.EXE

         a. Test 1 behaves as before

         b. Test 2 is different

      3. Preceeding a method declaration with virtual informs the compiler
         that:

         a. The class containing the declaration is likedly to be subclassed.

         b. Subclasses are likely to contain their own version of this method.

         c. When this method is invoked on a reference or pointer to an object
            of this class, the compiler should NOT make the choice of which
            actual method to call.  Instead, this choice should be deferred
            until run time, based on the actual type of the object that is
            pointed to or referenced.  The compiler generates the code needed
            to perform this check.
 
            i. Associated with every class that contains one or more virtual
               methods is a special compiler-created table called a VIRTUAL
               METHOD TABLE with one entry for every virtual method in the
               class, containing a pointer to the correct implementation of
               the method for that particular class.  (This table is sometimes
               called a vtable or vtbl.)

           ii. Each object that belongs to such a class contains a hidden
               field (sometimes called the vptr) that points to the table for
               that class.  (All objects of the same class share the same
               table).

          iii. When a virtual method is called, the compiler generates code
               that follows the objects vtpr to the table and gets the
               address of the correct method from the appropriate slot.

            TRANSPARENCY: Stroustrup Design/Evolution p. 75

         d. In contrast to the previous example (static or early binding of a 
            method to a name), this is known as DYNAMIC (or LATE) BINDING.

         e. It should be noted that "virtualness" is inherited.  If a method
            is declared as virtual in a base class, then it is also virtual
            in every class derived from this class, whether or not the word
            virtual is explicitly used.

            i. TRANSPARENCY - Piece hierarchy version 2

               Note omission of "virtual" from kind() method in subclasses -
               though it would not have been an error to include id.

           ii. It should be noted, though, that inheritance is always based
               on the signature of a method - e.g. if some subclass of Piece
               contained a kind() method that took one or more parameters
               it would not automatically be virtual.

      4. C++, then, gives the programmer two choices as to how the correct
         version of an overridden method is to be chosen

         a. Static (compile-time) binding: the choice is based on the declared
            type of the variable through which the method invocation is made.

         b. Dynamic (run-time) binding: the choice is based on the actual
            type of the object accessed through a reference or pointer.

         c. Note that static binding can always be used when a simple variable
            is used in the call; dynamic binding only becomes relevant when
            we have polymorphic references or pointers.

      5. In this regard, C++ differs from some other OO languages:

         a. In Smalltalk, dynamic binding is ALWAYS used.
        
         b. In Java, dynamic binding is used UNLESS the method being used
            is declared final - in which case it CANNOT be overloaded.

         c. The reason for making static binding the default in C++ is that
            it is slightly more efficient than dynamic binding - though the
            cost of method lookup in a vtable is relatively small.

      6. There is another issue we want to consider in conjunction with
         virtual methods.

         a. A virtual method must be declared as such in the base class that
            is common to all the classes that implement it.  This means, of
            course, that it must have some implementation for that class.

            Example: TRANSPARENCY - Piece hierarchy version 2

                     kind() is declared as a virtual method in class Piece,
                     and is implemented to return "?"

         b. Sometimes, though, the base class is intended to be abstract and
            there is not really sensible implementation of the method in that
            class.

            Example: The above

         c. In this case, it is possible to declare a PURE VIRTUAL or
            ABSTRACT method

            TRANSPARENCY - Piece hierarchy version 3

            i. The = 0 in the declaration for kind() in Piece informs the
               compiler that there will be no implementation of kind() for
               this class.  (The entry in the virtual method table for
               Piece for this method will not contain a meaningful value.)

           ii. A C++ class that contains one or more pure virtual methods
               is called an ABSTRACT CLASS, and can serve only as a base
               class for other classes.  In particular, it is impossible to
               construct an object that is of such a class directly.

      7. Finally, we should consider the possibility that a virtual method
         of a derived class may want to EXTEND the functionality of the base
         class method, not totally REPLACE it (as has been the case in our
         examples thus far.)

         a. Example: Suppose we build a class hierarchy for employees of a
                     corporation, some of whom are salaried and some of whom
                     are hourly:

                                        Employee

                        SalariedEmployee        HourlyEmployee

            i. All employees have a ssn, name, and address.
        
           ii. Salaried employees also have a salary, and hourly employees
               have an hourlyRate

         b. Now suppose we want to implement a print() method that prints out
            information on the employee.  For all employees, we will print
            SSN, name, and address.  For salaried employees, we will also
            print salary, and for hourly employees we will also print hourly
            rate.

            i. This method must be virtual if we are going to include
               employees in polymorphic data structures.

           ii. We could have each version implement the full functionality -
               i.e. the print() method for SalariedEmployee would contain
               four print operations.  But this involves duplicate work, and
               means that any change to the base class would require the
               print() methods of the derived class to change.

         c. The following is a better approach:

        void Employee::print()
          { cout << "SSN:     " << _ssn << endl;
            cout << "Name:    " << _name << endl;
            cout << "Address: " << _address << endl;
          }

        void SalariedEmployee::print()
          { Employee::print();
            cout << "Salary:  " << _salary << endl;
          }

        void HourlyEmployee::print()
          { Employee::print();
            cout << "Rate:    " << _hourlyRate << endl;
          }

        Note how the derived class methods explictly call the base class
        method.  Now any change to the way the base class prints itself
        will be automatically picked up by all the derived classes.

III. Operator Overloading in C++
---  -------- ----------- -- ---

   A. There is one last form of polymorphism in C++ that we need to consider:
      OPERATOR OVERLOADING.

      1. We have already seen that C++ allows method names to be overloaded.

      2. C++ also allows the built-in operators to be overloaded

   B. Example: In the ATM system, one of the most important data types is
      that used to represent money.  It overloads some of the arithmetic
      and comparison operators so that they can be applied to Money.  (More 
      could have been overloaded, but were not needed)

        TRANSPARENCY - money.h

        TRANSPARENCY - money.cc

        TRANSPARENCY - excerpt from bank.cc

   C. C++ allows almost any built-in operator to be overloaded, provided that

      1. The number of operands remains the same (e.g. ! always takes one
         operand, / always takes two operands, and - can be overloaded for
         either one operand, two operands, or both.)

      2. At least one operand is of a type different from the standard
         operator.  (You cannot redefine int + int, say)

         TRANSPARENCY: C++ Reference Manual 13.4

   D. C++ does not require the MEANING of the overloaded operator to be
      the same as the builtin one - e.g. += does not have to add the right
      operand to the left - it could be overloaded to always set the left
      operand to 0, say.  (But this would be poor design).

Copyright ©1998 - Russell C. Bjork