Virtual Methods and Polymorphism

📚 Gaddis (Ch. 15.4)

Redefining Base Class Methods

  • A method is said to be redefined in a derived class when it has the same name (and perhaps parameter list) as a method in the base class.
    • This is not the same thing as overloading.
  • Base class objects use the base class method… derived class objects use the derived class method.

Problem with Redefining

Consider this situation:

  • Class BaseClass defines methods x() and y().
    x() calls y().

  • Class DerivedClass inherits from BaseClass and redefines method y().

  • An object D of class DerivedClass is created and method x() is called.

  • When x() is called, which y() is used; the one defined in BaseClass or the the redefined one in DerivedClass?

#include <iostream>

class BaseClass{
  public:
    void x() {
        std::cout << "BaseClass::x()\n";
        y();
    }
    void y() {
        std::cout << "BaseClass::y()\n";
    }
};

class DerivedClass : public BaseClass {
  public:
    void y() {
        std::cout << "DerivedClass::y()\n";
    }
};

int main() {
    DerivedClass d1;
    d1.x();  // what is the output?
    return 0;
}

Problem with Redefining

Answer to the riddle:

  • The BASE class’s y() method is called!

    • This isn’t what you want…
    • but it should be what you expect…


  • From x() in the base class’s point-of-view (at compile time), the only available y() is its own.


  • Compiler binds the method call in x() to the base class y() at compile time… Once it’s done, it’s done.


  • This is called static binding

Class Hierarchies

  • A derived class can also be used as a base class.

  • This (of course) complicates issues like method redefining…

Virtual Methods

Virtual Method: a method in a base class that expects to be redefined in derived classes.

  • Defined with keyword virtual
virtual void y();
  • Allows dynamic binding – method name bound at runtime to the corresponding code. ( as opposed to static binding )
    • Dynamic binding is based on the type of the object actually initiating the call at runtime.
#include <iostream>

class BaseClass{
  public:
    void x() {
        std::cout << "BaseClass::x()\n";
        y();
    }
    virtual void y() {      // NOTE: virtual method
        std::cout << "BaseClass::y()\n";
    }
};

class DerivedClass : public BaseClass {
  public:
    virtual void y() {      // virtual because BaseClass::y() is...
        std::cout << "DerivedClass::y()\n";
    }
};

int main() {
    DerivedClass d1;
    d1.x();                // will work as desired now.
    return 0;
}

Polymorphism

A pointer of the base-class type may be “pointed” to a derived class object.

  • Remember the “is-a” relationship…

    • Base-class pointers can only access members defined in the base class.
    • This won’t work in reverse!
  • When the base class uses dynamic binding…

    • Base class pointer to derived class object behaves appropriately for the derived class.
    • This is called polymorphism.

Polymorphism requires a pointer or reference.

#include <iostream>

class BaseClass{
  public:
    void x() {
        std::cout << "BaseClass::x()\n";
        y();
    }
    virtual void y() {
        std::cout << "BaseClass::y()\n";
    }
};

class DerivedClass : public BaseClass {
  public:
    virtual void y() {
        std::cout << "DerivedClass::y()\n";
    }
};

int main() {
    BaseClass* d2 = new DerivedClass;
    d2->x();        // will behave as DerivedClass
    delete d2;
    return 0;
}

Redefining VS Overriding

Redefining: refers to statically-bound methods.

Overriding: refers to dynamically-bound methods.


  • Redefined methods do not exhibit polymorphic behavior

  • Overridden methods do exhibit polymorphic behavior.

When to Use virtual

Whenever you think a derived class might want to override a method!

  • Yes, there is a performance cost…
    • Dynamic binding requires a runtime “vtable” - a look-up table where the virtual function mapping is determined.
  • Destructors – It is (almost) always a good idea to make these virtual!
    • If a class might be used as a base class, it is recommended to make the destructor virtual.

Pure virtual Methods

Pure virtual method: a method that is not implemented (at all) in the base class, and is thus required to be overridden in derived classes.

  • Created by using “assignment”-style syntax:
virtual void y() = 0;
  • The “= 0” tells the compiler “this is a pure virtual method—don’t expect an implementation”.

Abstract Base Classes

Abstract base class: a base class that contains at least one pure virtual method.

  • Useful for factoring out common behavior from a family of objects.

    • For specifying a required common interface.
  • You cannot instantiate an object from an abstract base class.

  • You can create pointers to abstract base classes (useful for polymorphic behavior).

Multiple Inheritance

  • Multiple base classes
  • Inherit from all base classes (may result in multiple attributes/methods with same name)
  • Scope resolution will be required.

Multiple Inheritance

Here is what it looks like in a class diagram:

Multiple Inheritance Class Diagram

Example: Students and faculty.

class Student{                 | class Faculty{
public:                        | public:
    std::string   name;        |    std::string name;
    unsigned long id;          |    double      salary;
};                             | };

Notice the overlap - both classes need a name (and maybe other things as well).

Good programming practice would say we should “factor out” the common code…

Example: Students and faculty.

class Person{
public:
    std::string name;
};

class Student : public Person{ | class Faculty : public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

Now, we have the common code collected in a base class Person. Good!


But what if we want to add graduate assistants (GA’s)?


A GA is a student who also has some responsibilities similar to faculty. Could we use multiple inheritance?

Example: Students and faculty.

class Person{
public:
    std::string name;
};

class Student : public Person{ | class Faculty : public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

class GA : public Student, public Faculty {
public:
    std::vector<std::string> labs;
};


int main() {
    GA ga1;
    ga1.name = "Alice";  // Error!
}

Let’s look at the class diagram for this situation…

We started with a simple class hierarchy:

Person, Student, Faculty

Then we changed it so that a GA inherited from both Student and Faculty (which makes sense on some level).

Person, Student, Faculty


Notice the “diamond” shape this created, with Person as a common ancestor of both parent classes.

Notice the “diamond” shape this created, with Person as a common ancestor of both parent classes.

Person, Student, Faculty

This situation creates several challenges, and is known in programming circles as the Diamond of Death.

Back to the example…

class Person{
public:
    std::string name;
};

class Student : public Person{ | class Faculty : public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

class GA : public Student, public Faculty {
public:
    std::vector<std::string> labs;
};

int main() {
    GA ga1;
    ga1.name = "Alice";  // Error!
}
class Person{
public:
    std::string name;
};

class Student : public Person{ | class Faculty : public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

class GA : public Student, public Faculty
public:
    std::vector<std::string> labs;
};

int main() {
    GA ga1;
    ga1.Student::name = "Alice";  // this works... but...
}

We can use scope resolution… (but it is tedious, and doesn’t solve the redundancy)

class Person{
public:
    std::string name;
};
class Student : public Person{ | class Faculty : public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

class GA : public Student, public Faculty {
public:
    std::vector<std::string> labs;
};

int main() {
    GA ga1;
    ga1.Student::name = "Alice";  // this works... but...
    ga1.Faculty::name = "Bob";    // what about this?
    std::cout << "name is: "
        << ga1.Student::name
        << " - or is it - "
        << ga1.Faculty::name << "?\n";
}
class Person{
public:
    std::string name;
};
class Student : public Person{ | class Faculty : public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

class GA : public Student, public Faculty {
public:
    std::vector<std::string> labs;
};

int main() {
    GA ga1;
    ga1.Student::name = "Alice";  // this works... but...
    ga1.Faculty::name = "Bob";    // what about this?
    std::cout << "name is: "
        << ga1.Student::name            // will be "Alice"
        << " - or is it - "
        << ga1.Faculty::name << "?\n";  // will be "Bob"
}


Ouch.

The solution: Virtual Inheritance.

class Person{
public:
    std::string name;
};

class Student                  | class Faculty
: virtual public Person{       | : virtual public Person{
public:                        | public:
    unsigned long id;          |    double salary;
};                             | };

class GA : public Student, public Faculty {
public:
    std::vector<std::string> labs;
};

int main() {
    GA ga1;
    ga1.name = "Alice";  // now, we've really fixed it!
    std::cout << "Name is: " << ga1.name << ".\n";
}

Without virtual Inheritance:

Diamond Problem in a Physical Object

With virtual Inheritance

Solved Diamond Problem in a Physical Object

Virtual Methods and Polymorphism