Making Inheritance Work: C++ Issues

Steven Zeil

Last modified: Dec 19, 2017
Contents:

Recording

These slides accompany a recorded video: Play Video


1 Base Class Function Members

Even if you override a function, the inherited bodies are still available to you.

 class Person {
 public:
   string name;
   long id;
   void print(ostream& out) {
      out << name << " " << id << endl;}
 };
 
 class Student: public Person {
 public:
   string school;
   void print(ostream& out) {Person::print(out);
                       out << school << endl;}
 }

Base Class Constructors

This technique is often used in constructors so that subclasses will only need to initialize their own new data members:

 class Person {
 public:
   string name;
   long id;
   Person (string n, long i)
      : name(n), id(i)
   {}
 };
 
 class Student: public Person {
 public:
   string school;
   Student (string name, long id, School s)
      : Person(name, id), school(s)
   {}
 }

2 Assignment and Subtyping

2.1 Implementing Data Member Inheritance

Inheritance of data members is achieved by treating all new members as extensions of the base class:

 class Person {
 public:
   string name;
   long id;
 };
 
 class Student: public Person {
 public:
   string school;
 }


Extending Data Members

 


Assignment & Extension

 
In most OO languages, we can do

superObj = subObj;

but not

subObj = superObj;

3 Virtual Destructors

As we’ve seen, subclasses can add new data members.

What happens if we add a pointer:

gradStudent.cpp
class Person {
 public:
   string name;
   long id;
 };
 
 class Student: public Person {
 public:
   string school;
 }


class GraduateStudent: public Student {
private:
  Transcript* undergradRecords;
public:
   ...
   GraduateStudent (const GraduateStudent& g);
   GraduateStudent& operator= (const GradudateStudent&);
   ~GraduateStudent();
};

GraduateStudent::GraduateStudent (const GraduateStudent& g)
   : name(g.name), id(g.id), school(g.school),
     undergradRecords(new Transcript(*(g.undergradRecords))
{}

GraduateStudent& operator= (const GradudateStudent& g)
{
  if (this != &g)
    {
     Student::operator=(g);
     delete undergradRecords;
     undergradRecords = new Transcript(*(g.undergradRecords));
    }
   return *this;
}
   
GraduateStudent::~GraduateStudent() 
{
  delete undergradRecords;
}

and we don’t want to share?


Deleting Pointers and Inheritance

Consider the following two delete statements:

Person* g1 = new GraduuateStudent(...);
GraduateStudent* g2 = new GraduateStudent(...);
   ⋮
delete g1;  // compiler-generated ~Person() is called
delete g2;  // ~GraduateStudent() is called

Making the Destructor Virtual

The trick is that this has to be done at the top of the inheritance hierarchy

virtualDestruct.cpp
class Person {
 public:
   virtual ~Person() {}
   string name;
   long id;
 };
 
 class Student: public Person {
 public:
   string school;
 }


class GraduateStudent: public Student {
private:
   Transcript* undergradRecords;
public:
    ⋮
   GraduateStudent (const GraduateStudent& g);
   GraduateStudent& operator= (const GradudateStudent&);
   ~GraduateStudent();
};

even though,

4 Virtual Assignment

If subclasses can introduce new data members, should assignment be virtual so that we can guarantee proper copying of those extended data members?


Virtual Assignment Example

void foo(Person& p1, const Person& p2)
{
   p1 = p2;
}

GraduateStudent g1, g2;
  ⋮
foo(g1, g2);

What’s the Problem with Virtual Assignment?

If you try it, the inherited members aren’t what you might expect:

inherAsst.cpp
class Person {
 public:
   virtual ~Person() {}
   virtual Person& operator= (const Person& p);
   string name;
   long id;
 };
 
 class Student: public Person {
 public:
   string school;
   virtual Person& operator= (const Person& p); // inherited from Person
   // Student& operator= (const Student& s); // generated by compiler
 }


class GraduateStudent: public Student {
private:
   Transcript* undergradRecords;
public:
   ...
   GraduateStudent (const GraduateStudent& g);
   virtual Person& operator= (const Person& p); // inherited from Person
   // Student& operator= (const Student& s); // inherited from Student
   GraduateStudent& operator= (const GradudateStudent&);
   ~GraduateStudent();
};

You actually wind up with multiple overloaded assignment operators in the subclasses.


What’s the Problem with Virtual Assignment? (cont.)

void foo(Student& s1, const Student& s2)
{
   s1 = s2; 
}

Student s;
GraduateStudent g;
   ...
foo(g, s); // problem: s has no undergraduateRecords field

Recommendation

5 Virtual constructors


Example: evaluating a cell reference

cellrefnode.h
 class CellReferenceNode: public Expression
 { // represents a reference to a cell
 private:
   CellName value;
 
 public:
   CellReferenceNode () {}
     <:\smvdots:>
 
 // Evaluate this expression
   virtual Value* evaluate(const SpreadSheet&) const;
     <:\smvdots:>
 // Evaluate this expression
 
 Value* CellReferenceNode::evaluate(const SpreadSheet& s) const
 {
   Cell* cell = s.getCell(value);
   Value* v = (Value*)cell->getValue();
   if (v == 0)
     return new ErrorValue();
   else
     return v;
 }


Not like this!

 Value* theCopy = new Value(*v);


Better, but Not the “OO Way”

 Value* newCopy;
 if (typeid(*v) == typeid(NumericValue)) {
   newCopy = new NumericValue (v->getNumericValue());
 } else if (typeid(*v) == typeid(StringValue)) {
   newCopy = new StringValue (v->render(0));
 } else if (typeid(*v) == typeid(ErrorValue)) {
   newCopy = new ErrorValue();
   ⋮

(We’ll see how typeid works shortly.)

5.1 Cloning

Solution is to use a simulated “virtual constructor”, generally referred to as a clone() or copy() function.

 Value* CellReferenceNode::evaluate(const SpreadSheet& s) const
 {
   Cell* cell = s.getCell(value);
   Value* v = (Value*)cell->getValue();
   if (v == 0)
     return new ErrorValue();
   else
     return v->clone();
 } 

clone()

clone() must be supported by all values:

 class Value {
 public:
      ⋮
   virtual Value* clone() const;
      ⋮
 };


Implementing clone()

Each subclass of Value implements clone() as a copy construction passed to new.

 Value* NumericValue::clone() const
 {
   return new NumericValue(*this);
 }
 
 
 Value* StringValue::clone() const
 {
   return new StringValue(*this);
 }

6 Downcasting

Suppose that I want to be able to test any two Values to see if they are equal


Example: Value::operator==

We want to explicitly require all subclasses of Value to provide this test:

 class Value {
      ⋮
    virtual bool operator== (const Value&) const;
 };

The operator == compares two shapes. Its signature is: (const Value*, const Value&) => bool


Inheriting ==

 class NumericValue: public Value {
    ⋮
 class StringValue: public Value {
     ⋮

Both classes inherit the == operator. The signatures are

 (const NumericValue*, const Value&) =>  bool
 (const StringValue*, const Value&) => bool


Using the Inherited ==

 NumericValue n1, n2;
 StringValue s1, s2;
 bool b = (n1 == n2)
       && (s1 == s2)
       && (n1 == s1);

The last clause suggests a problem.


Implementing an asymmetric operator

We might implement == for NumericValue as:

 bool NumericValue::operator==
    (const Value& v)
 {
   return d == v.d;
 };


Implementing an asymmetric operator (cont.)

The problem is that we can easily define

 bool NumericValue::operator== (const NumericValue& v)

but

 bool NumericValue::operator== (const Value& v)

seems impossible, as we cannot anticipate all the values that will ever be defined.

6.1 RTTI


Working around the == asymmetry

The C++ standard defines a mechanism for RTTI (Run Time Type Information).

 bool NumericValue::operator== (const Value& v)
 {
   if (typeid(v) == typeid(NumericValue)) {
        const NumericValue &nv =
                    (const NumericValue&)v;
       return d == nv.d;
   } else
     return false;
 };


RTTI: typeid and downcasting

RTTI also allows you to test to see if v is from a subclass of NumericValue

if (typeid(NumericValue).before(typeid(v))

or to perform safe downcasting:

  NumericValue* np = dynamic_cast<NumericValue*>(&v);
  if (np != nullptr)
     {// v really was a NumericValue or
      //   subclass of NumericValue
          ⋮
     }


Downcasting Should Not Be a Crutch

Downcasting is often a tempting way to patch a poor initial choice of virtual “protocol” functions.

Oddly, though, downcasting is far more widely accepted in Java than in C++.

7 Single Dispatching & VTables


Equality Again

Earlier, we looked at the problem of comparing two spreadsheet Values:

class Value {
    ⋮
  virtual bool isEqual (const Value&) const;
};

class NumericValue: public Value {
    ⋮
  virtual bool isEqual (const Value&) const;
};

We saw that problems are caused by NumericValue::isEqual getting a parameter of type Value& rather than NumericValue&.


Why is this so hard?

Why can’t we select the best fit from among:

class NumericValue: public Value {
    ⋮
  virtual bool isEqual (const NumericValue&) const;
  virtual bool isEqual (const StringValue&) const;
  virtual bool isEqual (const ErrorValue&) const;
};

The answer stems from how dynamic binding is implemented.

7.1 Single Dispatching

Almost all OO languages offer a single dispatch model of message passing:


VTables

 


Compiling Virtual Function Declarations


Example of VTable Use

class A {
public:
  A();
  virtual void foo();
  virtual void bar();
};

class B: public A {
public:
  B();
  virtual void foo();
  virtual void baz();
};
  

foo(), bar(), and baz() are assigned indices 0, 1, and 2, respectively.

Consider the code:

A* a = ???;  // might point to an A or a B object
a->foo();


Example: VTable Structure

 

*(a->VTABLE[0])();
*(a->VTABLE[1])();

Notice that this works regardless of whether a points to an A object or a B object.


Implementing RTTI