Making Inheritance Work: C++ Issues

Steven Zeil

Last modified: Mar 03, 2014

Contents:
1. Base Class Function Members
2. Assignment and Subtyping
2.1 Implementing Data Member Inheritance
3. Virtual Destructors
4. Virtual Assignment
5. Virtual constructors
5.1 Cloning
6. Downcasting
6.1 RTTI
7. Single Dispatching & VTables
7.1 Single Dispatching

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

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

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

even though,

So you have to think ahead - if there’s any chance of a non-shared pointer being added in a future subclass, make your destructor virtual.

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

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

 // 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