Lecture Notes for Advanced Programming II

11 April 2002 - Implementing Subtype Polymorphism


  1. the look-up problem

    1. given the call i.n(), name look-up starts with class given in the declaration for i and goes up the inheritance hierarchy

      1. note this is the declared type, not the type of the value stored in i

      2. this type is known as the static type

        struct Number {
          void output(ostream &) const {
            cerr << "Number::output() not defined.\n";
            }
          };
        
        struct Complex : Number {
          void output(ostream &) const {
            cout << "Hello from Complex::output().\n";
            }
          };
        
        int main() {
          Number n;
          n.output(cout);             // prints "Number::output() not defined."
        
          Complex * cp = new Complex;
          cp->output(cout);           // prints "Hello from Complex::output()."
        
          Number * np = cp;
          np->output(cout);           // prints "Number::output() not defined."
          }
        

    2. given subtype polymorphism, look-ups that start from the static type are not helpful

      1. the correct routine is below (a descendent of) the static type

      2. np has static type Number *, np->output(cout) calls Number::output()

      3. want to call Complex::output()

  2. virtual functions and the static look-up problem

    1. static-type look-ups start too high in the inheritance hierarchy

    2. the name look-up should start with the type of the value being held in a variable, not with the type of the variable - the dynamic type

    3. dynamic-type look-ups start lower in the hierarchy than do static-type look-ups - both go up

    4. a function with the virtual keyword indicates the function should be looked-up starting at the dynamic type

      struct Number {
        virtual void output(ostream &) const {
          cerr << "Number::output() not defined.\n";
          }
        };
      
      struct Complex : Number {
        void output(ostream &) const {
          cout << "Hello from Complex::output().\n";
          }
        };
      
      int main() {
        Number n;
        n.output(cout);             // prints "Number::output() not defined."
      
        Complex * cp = new Complex;
        cp->output(cout);           // prints "Hello from Complex::output()."
      
        Number * ap = cp;
        ap->output(cout);           // prints "Hello from Complex::output()."
        }
      

    5. without the virtual keyword, look-up starts at the static type.

    6. virtual is a per-function keyword - a class can mix virtual and non-virtual functions

      struct Number {
        virtual void output(ostream &) const {
          cerr << "Number::output() not defined.\n";
          }
        void print_type(void) const {
          cout << "A generic number.\n";
          }
        };
      
      struct Complex : Number {
        void output(ostream &) const {
          cout << "Hello from Complex::output().\n";
          }
        void print_type(void) const {
          cout << "A complex number.\n";
          }
        };
      
      int main() {
        Number n;
        n.output(cout);             // prints "Number::output() not defined."
        n.print_type();             // prints "A generic number."
      
        Complex * cp = new Complex;
        cp->output(cout);           // prints "Hello from Complex::output()."
        cp->print_type();           // prints "A complex number."
      
        Number * ap = cp;
        ap->output(cout);           // prints "Hello from Complex::output()."
        ap->print_type();           // prints "A generic number."
      
        }
      

    7. all descendents of a virtual function are virtual - no virtual keyword necessary

      struct Number {
        virtual void output(ostream &) const {
          cerr << "Number::output() not defined.\n";
          }
        void print_type(void) const {
          cout << "A generic number.\n";
          }
        };
      
      struct Floating_point : Number {
        void output(ostream &) const {
          cout << "Hello from Floating_point::output().\n";
          }
        void print_type(void) const {
          cout << "A floating-point number.\n";
          }
        };
      
      struct Transfinite : Floating_point {
        void output(ostream &) const {
          cout << "Hello from Transfinite::output().\n";
          }
        void print_type(void) const {
          cout << "A transfinite number.\n";
          }
        };
      
      int main() {
        Number n;
        n.output(cout);    // prints "Number::output() not defined."
        n.print_type();    // prints "A generic number."
      
        Floating_point * fp = new Floating_point;
        fp->output(cout);  // prints "Hello from Floating_point::output()."
        fp->print_type();  // prints "A floating-point number."
      
        Number * np = fp;
        np->output(cout);  // prints "Hello from Floating_point::output()."
        np->print_type();  // prints "A generic number."
      
        delete fp;
        fp = new Transfinite;
        fp->output(cout);  // prints "Hello from Transfinite::output()."
        fp->print_type();  // prints "A floating-point number."
      
        np = fp;
        np->output(cout);  // prints "Hello from Transfinite::output()."
        np->print_type();  // prints "A generic number."
        }
      
      

    8. ancestors of a virtual function are not virtual, unless explicitly declared so

      struct Number {
        void output(ostream &) const {
          cerr << "Number::output() not defined.\n";
          }
        };
      
      struct Complex : Number {
        virtual void output(ostream &) const {
          cout << "Hello from Complex::output().\n";
          }
        };
      
      int main() {
        Number n;
        n.output(cout);         // prints "Number::output() not defined."
      
        Complex * cp = new Complex;
        cp->output(cout);       // prints "Hello from Complex::output()."
      
        Number * np = cp;
        np->output(cout);       // prints "Number::output() not defined."
        }
      
      

  3. pointers and references

    1. Dynamic look-up only works if class instances are accessed indirectly via references or pointers

      struct Number {
        virtual void output(ostream &) const {
          cerr << "Number::output() not defined.\n";
          }
        };
      
      struct Complex : Number {
        void output(ostream &) const {
          cout << "Hello from Complex::output().\n";
          }
        };
      
      int main() {
        Number n;
        n.output(cout);  // prints "Number::output() not defined."
      
        Complex c;
        c.output(cout);  // prints "Hello from Complex::output()."
      
        Number & nr = c;
        nr.output(cout); // prints "Hello from Complex::output()."
        }
      

    2. Assigning a child value to a parent variable chops the child off at the parent-level members

      int main() {
        Number n;
        n.output(cout);  // prints "Number::output() not defined."
      
        Complex c;
        c.output(cout);  // prints "Hello from Complex::output()."
      
        n = c;	   // c is chopped to fit n.
        n.output(cout);  // prints "Number::output() not defined."
        }
      

  4. fine points

    1. virtual destructors

      1. what happens when a Complex is assigned to a Number and then goes out of scope

      2. if Number::~Number is not virtual, static-type look-up is used - Complex::~Complex is never called.

      3. classes defining virtual functions almost always need to define a virtual destructor

        1. the virtual destructor needn't do anything - it just forces dynamic-type look-up

        2. this is separate from dynamic memory - even without dynamic memory, a class with a virtual function needs a virtual destructor

        3. compare this with the rule of three

    2. virtual function definitions

      1. a virtual function must always be defined - even if it's never called

      2. this is different from regular member functions

      3. compilers usually aren't helpful on this error

        $ cat t.cc
        #include 
        
        using std::cerr;
        using std::cout;
        
        struct Number {
          virtual void output(ostream &);
          };
        
        struct Complex : Number {
          void output(ostream &) const {
            cout << "Hello from Complex::output().\n";
            }
          };
        
        int main() {
          Complex c;
          c.output(cout);  // prints "Hello from Complex::output()."
          }
        
        
        $ g++ -o t t.cc
        /var/tmp/ccSKs5j4.o(.gnu.linkonce.d._vt.7Complex+0xc): 
          undefined reference to Number::output(ostream &)
        /var/tmp/ccSKs5j4.o: In function Complex type_info function:
        /var/tmp/ccSKs5j4.o(.gnu.linkonce.t.__tf7Complex+0x1c): 
          undefined reference to Number type_info function
        /var/tmp/ccSKs5j4.o(.gnu.linkonce.t.__tf7Complex+0x34): 
          undefined reference to Number type_info node
        /var/tmp/ccSKs5j4.o(.gnu.linkonce.t.__tf7Complex+0x38): 
          undefined reference to Number type_info node
        /var/tmp/ccSKs5j4.o: In function Number::Number(void):
        /var/tmp/ccSKs5j4.o(.Number::gnu.linkonce.t.(void)+0x8): 
          undefined reference to Number virtual table
        /var/tmp/ccSKs5j4.o(.Number::gnu.linkonce.t.(void)+0xc): 
          undefined reference to Number virtual table
        collect2: ld returned 1 exit status
        
        $ CC -c t.cc
        "t.cc", line 15: Warning: Complex::output hides the virtual function
        Number::output(std::basic_ostream>&).
        1 Warning(s) detected.
        
        $
        

      4. be careful how you define the virtual function

        1. accidently calling the virtual function is an annoying error to find

        2. have the virtual function blow-up when called

          struct Number {
            virtual void output(ostream &) {
              cerr << "Trying to call Number::output().\n";
              abort();
              }
            };
          

    3. absolute virtual functions

      1. defining virtual functions can be a pain - particularly if there's a lot of them

      2. also, it's easy to forget to redefine a virtual function in a child class

      3. also, defining a variable of a virtual function class may not make sense - particularly if the virtual functions explode

      4. abstract virtual functions take care of all these problems

      5. an abstract virtual function is a virtual function initialized to zero (this is disgusting syntax)

        struct Number {
          virtual void output(ostream &) = 0;
          };
        

      6. a class containing an abstract virtual function is an abstract virtual class

      7. it is not possible to declare a variable for an abstract virtual class

        $ cat t.cc
        #include 
        
        using std::ostream;
        
        struct Number {
          virtual void output(ostream &) = 0;
          };
        
        int main() {
          Number n;
          }
        
        $ g++ -c t.cc
        t.cc: In function int main():
        t.cc:12: cannot declare variable n to be of type Number
        t.cc:12:   since the following virtual functions are abstract:
        t.cc:8: 	void Number::output(ostream &)
        
        $ CC -c t.cc
        "t.cc", line 12: Error: Cannot create a variable for abstract class Number.
        1 Error(s) detected.
        
        $
        

      8. defining pointers and references to abstract virtual classes is ok

        struct Number {
          virtual void output(ostream &) const = 0;
          };
        
        struct Complex : Number {
          void output(ostream &) const {
            cout << "Hello from Complex::output().\n";
            }
          };
        
        int main() {
          Complex c;
          Number & nr = c;
          Number * np = &c;
        
          nr.output(cout);    // Prints "Hello from Complex::output()."
          np->output(cout);   // Prints "Hello from Complex::output()."
          }
        

      9. abstract behaves like virtual - a child function is abstract unless its defined; no need to use abstract again in the child

        1. forgetting to redefine a child makes the child abstract - a relatively easy error to catch

          $ cat t.cc
          #include 
          
          using std::cerr;
          using std::ostream;
          
          struct Number {
            virtual void output(ostream &) const = 0;
            };
          
          struct Complex : Number {
            // output() is still abstract virtual.
            };
          
          int main() {
            Complex c;
            Number & nr = c;
            Number * np = &c;
          
            nr.output(cout);    // Prints "Hello from Complex::output()."
            np->output(cout);   // Prints "Hello from Complex::output()."
            }
          
          $ g++ -c t.cc
          t.cc: In function int main():
          t.cc:15: cannot declare variable c to be of type Complex
          t.cc:15:   since the following virtual functions are abstract:
          t.cc:7: 	void Number::output(ostream &) const
          
          $
          

    4. Be careful how you match virtual parent and child function prototypes

      1. if the two prototypes mismatch, they aren't the same function

      2. that means the child isn't virtual (or abstract virtual) either

        struct A {
          virtual void hi(unsigned) {
            cout << "Hello from A:hi().\n";
            }
          virtual void bye(int) {
            cout << "Bye from A:hi().\n";
            }
          };
        
        struct B : A {
          void hi(int) {
            cout << "Hi from B:hi().\n";
            }
          void bye(int) {
            cout << "Bye from B:bye().\n";
            }
          };
        
        int main() {
          B b;
          A * ap = &b;
        
          ap->hi(0);   // an int isn't an unsigned; prints "Hello from A:hi()."
          ap->bye(0);  // prints "Bye from B:bye()."
          }
        
        

      3. the const function modifier takes part too

        struct A {
          virtual void hi(void) {
            cout << "Hello from A:hi().\n";
            }
          virtual void bye(void) const {
            cout << "Bye from A:bye().\n";
            }
          };
        
        struct B : A {
          void hi(void) const {
            cout << "Hi from B:hi().\n";
            }
          void bye(void) const {
            cout << "Bye from B:bye().\n";
            }
          };
        
        int main() {
          B b;
          A * ap = &b;
        
          ap->hi();   // hi() const isn't hi(); prints "Hello from A:hi()."
          ap->bye();  // prints "Bye from B:bye()."
          }
        


This page last modified on 22 April 2002.