Week 13

Class Relationships

Value-Oriented or Object-Based programming involves creating new, user-defined types. There are four strategies for building a new type:

  • Build it completely from scratch, using only built-in components.
  • Build it from scratch, but make use of the classes that others have written to do some of the work.
  • Combine simpler types to create complex types. This is composition.
  • Extend a general class, adding new features. This is called inheritance.

Programming with inheritance is called Object Oriented Programming.

These strategies express three kinds of "class relationships":

  • The uses-a relationship, (or association), occurs when your class uses the services of other classes. For instance, if your class uses cout in one of your member functions, your class is dependent on the ostream class.
  • The has-a relationship, says one class is a combination of other objects. In the has-a relationship one type is composed of different parts. A Bicycle class thus may contain two instances of the Wheel class.
  • The is-a relationship, when one class is an extension or "kind of" another class. The is-a relationship occurs when members of one class are a subset of another class. The is-a relationship is implemented using public inheritance. In the relationship shown here, we'd say that a MountainBike is-a Bicycle. An inheritance hierarchy.
Week 13

Polymorphic Inheritance

Public inheritance is a form of specialization. The derived class inherits both the member functions and the data members from the base class, while optionally adding more of both. The derived class IS-A specialized form of the more general base class.

A derived class may override a virtual member function to add specialized behavior, as we did with Student::toString(). This is called polymorphic inheritance, it provides specialized behavior in response to the same messages.

The running man icon. Let's see if that's true. Let's use our simple Person<-Student hierarchy from the last few lessons and see what happens with some experiments. Click the Running Man on the left to open a copy of the lab for this lesson. Make sure you Fork it so that you have your own copy.

Change toString() in each class so it identifies the class at the beginning of the method. Here are the modified toString() member functions. Notice that this version of the Student::toString() no longer calls its base class version; it entirely replaces it.

string Person::toString() const
{
    return "Person::Name: " + name;
}

string Student::toString() const
{
    return "Student::Name: " + getName()
        + ", ID: " + to_string(studentID);
}
Week 13

Static Polymorphism

Use make run to see the main program running. This is a kind of "polymorphism", known as static polymorphism. You send the same message to different objects and each responds to the same message according to its nature, just like the duck, dog and cat in the picture. Animals speaking polymorphically.

This is not what we mean when we talk about polymorphism. This would work exactly the same even if Person and Student were completely unrelated classes.

What we mean by polymorphism is an inheritance relationship where the request can be sent to any kind of Person object, and the specialized Person, such as a Student or an Employee responds appropriately.

Week 13

A Perplexing Problem

Change the example (main.cpp) again, so it looks like this:

int main()
{
    Person sam = Student("Sam", 201795);
    Person pam = Person("Pam B.");
    
    cout << "sam says->" << sam.toString() << endl;
    cout << "pam says->" << pam.toString() << endl;
}

Now you have two Person objects: one "regular" Person, and one specialized Person who is a Student. It the output the same as previously? No!!!

Running the new program.

For the Student sam, you know longer see the ID. And, both the Student and the Person are identified with Person::Name, even though we do have an overridden member function, Student::toString().

Why does this happen?
Week 13

The Slicing Problem

Here's why this happens. First, objects in C++ are value types, unlike the reference types in Java. When you assign a derived class object to a base class variable, only the base class portion of the object is copied. This is called the slicing problem. Illustrating the slicing problem.

If you pass a derived class object by value to a function that expects a base class object, the same slicing will occur as well. This is easy to fix. Just always follow this rule:

Never ever ever ever assign a derived class object to a base class variable. Ever!
Week 13

References & Pointers

While slicing is a problem it is not the only culprit here. Even without slicing, the code would still not work because in C++ polymorphism only works with references or pointers.

To see, this, make the following changes to main.cpp:

Student sam = Student("Sam", 201795);
Person pam = Person("Pam B.");
Person& samRef = sam;
Person* samPtr = &sam;
cout << "sam says->" << samRef.toString() << endl;
cout << "sam says->" << samPtr->toString() << endl;
cout << "pam says->" << pam.toString() << endl;

Now, the Person& reference samRef refers to the Student object sam, and when we call samRef.toString() it calls Student::toString(), not Person::toString() like our previous examples did.

The same thing happens if we use the Person* samPtr. It is also polymorphic.

Week 13

Polymorphic Functions

What we really want are polymorphic functions like this:

// A polymorphic function
void greet(const Person& p)     // any kind of Person
{
    cout << "Hello, I'm " << p.toString() << endl;
}

This function is polymorphic because the formal parameter is a reference to a base class. (Note, not a base class object.) You can pass any kind of Person, such as a Student or an Employee object and it will behave appropriately.

Polymorphic functions should operate on references or pointers to a base class. Functions should never use pass-by-value with base class objects.

Week 13

Polymorphic Lists

Creating a list (vector or array) of different kinds of object also leads to slicing:

vector<Person> v;
v.push_back(Student("Sam", 201795));    // OOPS!!!
v.push_back(Person("Pam B."));

When you push_back a Student or Employee object, the object is sliced when it is copied into the vector. The vector v does not contain a Student and a Person; it contains two Person objects. Sam has been stripped of everything that makes him a Student; he has been effectively lobotomized; he no longer knows who he is.

You also cannot fall back on using references, like you did with polymorphic functions, since you cannot create a vector<Person&> v or an array, Person& a[3]. Both of these declarations are illegal. A reference is not a variable or object (lvalue), but an alias for an existing lvalue.

Week 13

Pointers to the Rescue

One solution is to create a vector<Person*> v or an array Person* a[2]. Here's a short example that places two different kinds of Person pointers in a vector and prints them. Each person responds appropriately. Go ahead and add this code to main(). Include the <vector> header.

int main()
{
    vector<Person*> people;
    people.push_back(new Student("Sam", 201795));
    people.push_back(new Person("Pam B."));
    
    for (auto p : people) {cout << p->toString() << endl;}
    for (auto p : people) delete p;
}

Since two of these objects are created on the heap, it is up to you to reclaim their memory before the vector goes out of scope and it is lost. The vector cannot do it because it does not know if the pointers it contains point to objects on the heap or objects on the stack. If you add a stack-based pointer to this program, it crashes.

Week 13

Maybe Smart Pointers?

You can eliminate the need to reclaim memory by using the two C++11 smart pointers, shared_ptr and unique_ptr, which are declared in the header <memory>.

Here is a version of the program that uses unique_ptr, which doesn't require the delete loop at the end.

using up = unique_ptr<Person>;
vector<up> people;
people.push_back(up{new Student("Sam", 201795)});
people.push_back(up{new Person("Pam B.")});
for (const auto& p : people) cout << p->toString() << endl;

The unique_ptr constructor is explicit, so a type alias helps to shorten the name. Also, unique_ptr objects cannot be copied, so the range-based for loop accesses each element by const reference, which is what you need anyway.

Because the vector now stores only smart pointers, you can't inadvertently add a stack-based pointer to the list.