Re-Use

As Functions and Classes are introduced in these notes, we have also introduced the software engineering concept of modularity. Functions provide the principal mechanism for modularizing C programs, while both functions and classes provide mechanisms for modularizing C++ programs. Modularity using functions is known as procedural programming, and modularity using classes is known as object based programming. We now indroduce two other important concepts in software engineering: re-use of code and interface consistency.

Code re-use happens when a particular segment of actual code can be used in more than one program or more than one place in a program. Code re-use is an important part of software development that is facilitated by many mechanisms, including functions. Classes also contribute to code re-use through the mechanism of inheritance. On its face, re-using code is a good idea, provided of course that the code to be re-used is correct and high-quality. One of the great dis-services that software engineers can do is to create faulty or inefficient code and offer it for re-use to other programmers. On the other hand, when a software group supplies high quality code for others to re-use, the benefits are enormous. The benefit from the effort to create the re-usable code is amplified many fold as the code is put to use throughout the user community.

Interface consistency is another kind of re-use, wherein an interface for a type is defined and then used consistently across an entire family of related types. Clearly code re-use enhances correctness of software and productivity of software developers by offering code that is written and tested in advance. Interface consistency enhances correctness and productivity by establishing a common expectation of how things work so programmers don't have to look up names of utilities.

Class Inheritance

If we have one user type and want to create another more specialized type, we could simply copy the original class definition and add to it to create the sub-type. For example, suppose we have the type Solid and want to create some specialized types Box, Cylinder, and Sphere and want to enforce a consistent public interface for these and possibly other solid shapes that might arise in the future. We can accomplish this by defining a class for type Solid and deriving the more specific shapes from the base type as sub-types. Solid might be defined as follows:

class Solid
{
  public:
    const char* SolidType() const;
    float       Volume() const;
};

where SolidType() is intended to identify the particular category of solid, such as "Box" or "Cylinder", and Volume() gives the volume of the solid. Then we could implement the Solid member functions as follows:

const char* Solid::SolidType() const
{
  return "generic";
}

float Solid::Volume() const
{
  return 0.0;
}

Knowing nothing about the solid, it seems sensible for Volume() to return zero, although a better solution is simply not to implement this function at all. The class Solid is not particularly useful directly by client programs. It does however provide an important feature for the use of sub-types of Solid: a common public interface for these sub-types.

Now we can define the sub-types as follows:

class Box : public Solid
{
  public:
    Box(float l = 1, float w = 1, float h = 1)
    float Volume() const;
    const char* SolidType() const;
  private:
    float length, width, height;
}; 

Box::Box (float l, float w, float h) : length(l), width(w), height(h)
{}

float Box::Volume() const
{
  return length*width*height;
}

const char* Box::SolidType() const
{
  return "box";
}

The class Box is said to be derived from Solid and Solid is called a base class for Box. Other terminology in common use is to call Solid the parent class of Box and Box a child class of Solid. The lineage analogy continues: any class that is up the chain of derivations (parent of a parent...) is called an ancester and any class that is down the chain (child of a child ...) is called a descendant.

Inherit/Override Methods

The method prototypes float Volume() and const char* SolidType() const are inherited by the derived classes. The method implementations may be overridden in the derived class or they may be used as inherited, that is, we may choose to re-implement (override) a method in the derived class or re-use (inherit) the implementation from the base class. Note the distinction between inheriting the prototype and inheriting the implementation: the inheritance of the prototype is automatic and not optional, while the inheritance of the implementation is optional. Here is an example:

class Cube : public Box
{
  public:
    Cube(float side);
    const char* SolidType() const;  // override
};

The derived class Cube has a constructor that takes a single argument specifying the side length of the cube. There are two inherited method prototypes, float Volume() and const char* SolidType() const. However only one of these is mentioned in the derived class definition. This is the one whose implementation needs to be overriden. Thus, by prototyping SolidType in the derived class we signal the need to override its implementation, and by omitting mention of Volume in the derived class we signal that we will re-use the implementation by inheriting from the base class. The implementation of Cube would go as follows:

Cube::Cube(float side) : Box(side, side, side)
{}

const char* Cube::SolidType() const
{
  return "cube";
}

Note that the Cube constructor calls the parent class constructor Box() in its initialization list and that SolidType() needs a new implementation to report a correct solid type for Cube. There is no need to re-write an implementation for Volume() because the volume of a cube is the same as its volume as a box:

L x W x H = S x S x S = S3

where L = length, W = width, H = height, and S = side.

A minor problem with this scheme is that humans can forget what the methods of a class are when they are not mentioned in the class definition (because they are being inherited as implemented from the base class). This problem is alleviated by good documentation. In practice, while developing a class hierarchy, it may be helpful to repeat all of the methods in the derived class, commenting out those whose implementations we intend to inherit, as in:

class Cube : public Box
{
  public:
    Cube(float side);
    // float Volume () const;       // inherit
    const char* SolidType() const;  // override
};

These comment lines can be removed after development to clean up the code.

Scope Resolution: Accessing Members of Parent Class

It can happen that the implementation of a derived class needs to call both the overridden and the original version of a member function. These methods will have the same prototype, so it is impossible for the usual compiler-supported process to distinguish them. Fortunately these are in different scopes.

The implementation of a class member function, from the name of the function through the end of the body, code is declared to be inside the class namespace. For example, in the implementation

const char* Cube::SolidType() const
{
  return "cube";
}

the return value of type const char* is outside the class scope, but then the notation Cube:: asserts that the following code, through the end of the implementation block (the last closing brace "}"), the code is inside the Cube namespace. Thus, in that context, a call to a method is assumed to be the one in that same namespace. To call the same method from the parent class, one uses explicit scope resolution. This is illustrated in the following ShowLineage() method that displays the types of the class, its parent, and its grandparent:

void Cube::ShowLineage()
{
  std::cout << SolidType() << '\n'
            << Box::SolidType() << '\n'
            << Solid::SolidType() << '\n';
}

We could use the current class scope explicitly as well, if that improved code readability:

void Cube::ShowLineage()
{
  std::cout << Cube::SolidType() << '\n'
            << Box::SolidType() << '\n'
            << Solid::SolidType() << '\n';
}

Of course, we also would need to add the prototype of ShowLineage() to the class definition.

Constructors and Destructors

Typically, classes need a variety of constructors and a destructor, and derived classes are not an exception. The relationships between constructors and destructors of the derived and their base class counterparts is important to the correct functioning of inheritance. Of course, the base class versions are inherited, but the import of inheritance of constructors and destructors is subtle, because these methods are rarely called explicitly.

Constructors and Inheritance

There is a basic rule, enforced by some compilers: If a parent class has a constructor explicitly defined, derived class constructors should call an appropriate parent class constructor. Moreover, this call should be made in an initialization list. Such is illustrated above in the constructor for class Cube and again below for class Carton.

Note: In cases where the parent class has a default constructor, the compiler will automatically call the parent default constructor. This is the one situation where making an explicit call to a parent constructor is not necessary, becuase it happens implictly.

Note that this rule ensures that constructors are called in bottom-up order in the inheritance hierarchy, beginning with the type being created and ending at the initial base class (or classes - see multiple inheritance, below). However, the constructors execute in top-down order. Thus the base class is "built" before the derived class constructor executes. This is sort of a build/remodel process: The base class constructor call creates the basic structure, then the derived class constructor remodels according to its specialized needs. Making the call(s) to base class constructors in an initialization list ensures that the derived class constructor will have a complete base class to work with before the body of the derived class constructor begins executing.

Destructors and Inheritance

Destructors are called automatically when a variable goes out of scope. Because the base class destructor is inherited, and because the derived class object "is" a base class object, both the derived class destructor (even if it is the "default" destructor) and the base class destructor are called automatically. The order in which these are called is bottom-up, and the destructors run to completion before the parent destructor is called. Thus the destructors execute in bottom-up order.

Code Example

class Base
{
  public:
    Base  ()
    {
      std::cout << "Base::Base()\n";
    }
    ~Base ()
    {
      std::cout << "Base::~Base()\n";
    }
};

class Derived : public Base
{
  public:
    Derived  () : Base() 
    {
      std::cout << "Derived::Derived()\n";
    }
    ~Derived ()
    {
      std::cout << "Derived::~Derived()\n";
    }
};

The parent and child classes do no more than output a statement identifying the function call. Note the call to the base class constructor in the initialization list for the derived class constructor.

Here is a client program that does no more than create variables and then let them go out of scope:

int main()
{
  Base b;
  Derived d;
  return 0;
}

The output from compiling and running this program is as follows:

Base::Base()
Base::Base()
Derived::Derived()
Derived::~Derived()
Base::~Base()
Base::~Base()

Note the execution sequence for the constructors and destructors. Those emphasized in color are the result of the declaration of the derived class object.

Copy Constructors and Inheritance

Copy constructors can be thought of as a 1-parameter constructors, and as such the constructor/inheritance discussion above applies: the implementation of a derived class copy constructor should call the base class copy constructor explicitly in its initialization list.

Summary

Constructors are called bottom-up by placing explicit parent class constructor calls in the initialization list of the derived class constructor implementation. Because the calls are nested and occur prior to entering the implementation body, constructor bodies execute in top-down order.

The above applies in particular to copy constructors.

Parent class destructors are not called explicitly by derived class implementations, and destructors are called automatically in bottom-up order. Because these are not nested calls, destructor bodies execute in bottom-up order.

Inheritance of Member Variables

All class members, both variables and method prototypes, of the base class are inherited by the derived class. In particular, variables of the base class also exist in the derived class. When inheritance is public (or protected - see discussion below) then any public variable in the base class can be accessed directly by derived class objects. However, private variables of the base class can not be accessed directly by the derived class, even inside its implementation. In many cases, direct access is not needed, because appropriate indirect access had been built in to the base class via its own public methods (which are inherited).

We have already seen an example of a derived class method accessing private base class variables can be accessed indirectly in the constructor for Cube above, where the private class variables are initialized through a call to the base class constructor:

Cube::Cube(float side) : Box(side, side, side)
{}

Of course, the private base class data is also accessed by the inherited method Volume(), which we did not override for the derived class.

Access Control and Inheritance

There are times when a derived class needs access to base class variables or methods that still require restricted access for client programs. The keyword protected is an access control keyword that serves this purpose exactly: It allows derived class access but prevents client access, that is it means "public" for the derived class and "private" for client programs. Here is an example, where we add method Cube::SetSide(s) to the child class:

class Box : public Solid
{
  public:
    Box(float l = 1, float w = 1, float h = 1)
    float Volume() const;
    const char* SolidType() const;
  protected:
    float length, width, height;
}; 

Note that we have changed the keyword private to protected. Now we can add SetSide() to the derived class:

class Cube : public Box
{
  public:
    Cube(float side);
    const char* SolidType() const;  // override
    void SetSide(float s);
};

void Cube::SetSide (float s)
{
  width = s;   // access protected base class variable
  length = s;  // access protected base class variable
  height = s;  // access protected base class variable
}

Note that we added the prototype SetSide() and that its implementation uses access to protected member variables inherited from the base class. This access would be an error if we had not changed private to protected in the base class.

We could have planned the implementations differently and avoided the child class access to base class variables by also adding a method SetDimensions() to the base class:

class Box : public Solid
{
  public:
    Box(float l = 1, float w = 1, float h = 1)
    float Volume() const;
    const char* SolidType() const;
    void SetDimensions(float l, float w, float h);
  private:
    float length, width, height;
}; 

void Box::SetDimensions (float l, float w, float h)
{
  length = l;
  width = w;
  height = h;
}

Note that we have changed the keyword protected back to private. We also override SetDimensions() just for logical consistency with the following changes to the derived class:

class Cube : public Box
{
  public:
    Cube(float side);
    const char* SolidType() const;  // override
    void SetDimensions(float l, float w, float h);
};

void Cube::SetDimensions (float l, float w, float h)
{
  h = w = l;  // make sure parameters are equal
  Box::SetDimensions(l, w, h);
}

The implementation of Cube::SetDimensions() no longer requires access to base class variables. Accessing them indirectly through a base class public member function is perfectly OK - even a client program has such permissions.

There are also two more categories of inheritance to go with public: protected and private. Combined with the three categories of access by the same names, this leads to a dizzying array (in fact, a 3-dimensional array) of possible access control limitations for classes, their client programs, and their derived classes.

For any choice of derivation category and access category, there are three questions to answer:

  1. What are the access rights of the derived class to base class members?
  2. What access rights can the derived class pass on to its children?
  3. What are the access rights of clients of the derived class?

The 27 answers to these three questions are given in the following three tables.

Derived Class Implementations Access Base Class Members?
Base Class Access CategoryInheritance Category
publicprotectedprivate
publicyesyesyes
protectedyesyesyes
privatenonono

Implementation Access Rights Passed to Children?
Base Class Access CategoryInheritance Category
publicprotectedprivate
publicyesyesno
protectedyesyesno
privatenonono

Clients of Derived Class Access Members of Base Class?
Base Class Access CategoryInheritance Category
publicprotectedprivate
publicyesnono
protectednonono
privatenonono

Note that the general rule is that higher restrictions trump lower restrictions. Here are summaries of the nine possible variations:

Many of these possibilities are of limited appeal.

Multiple Inheritance

C++ provides a mechanism to implement multiple inheritance, wherein a class to derives from more than one base class. We will illustrate this phenomenon by creating a hierarchy of classes matriarched by the base class Material and then defining Carton as a class derived from both Box and Cardboard. First, the Material classes:

class Material
{
  public:
    Material (float density, float strength, float cost);
    const char* Name     () const;
    float       Density  () const;
    float       Strength () const;
    float       Cost     () const;
    ...

  private:
    float my_density, my_strength, my_cost;
    ...
};

In this admittedly simplified description of a generic material, we assume that it has a Name (such as "steel", "wood", "cardboard"), Density (weight per unit area), Strength (breaking strength per unit area), Cost (per unit area), and so on. There would likely be access and set methods for a variety of class variables. Then a specific materail would be a derived type, such as Cardboard:

class Cardboard : public Material
{
  public:
    Cardboard (float density, float strength, float cost);
    const char* Name();
};

Only the constructor Cardboard() and Name() require implementations. The constructor is implemented with a call to the base class constructor and an empty body. The override of Name() is necessary to correctly identify the derived material:

Cardboard::Cardboard (float density, float strength, float cost)
  : Material(density, strength, cost)
{}

const char* Cardboard::Name() const
{
  return "cardboard";
}

We are now positioned to illustrate multiple inheritance with the class Carton:

class Carton : public Box, public Cardboard
{
  public:
    Carton (float l, float w, float h, float density, float strength, float cost);
};

Carton::Carton (float l, float w, float h, float density, float strength, float cost)
  : Box(l, w, h) , Cardboard (density, strength, cost)
{}

Thus a Carton is a box and is cardboard - an intuitively acceptable computing model, assuming it matches the enterprise concept of a carton. Note that the derived class has two parents, that both parent constructors are called in the initialization list for the derived class constructor, and that the parameters are listed in the order in which the parent classes are listed in the derivation.

Operators, Inheritance, and Upcasting

Most operators that are defined for a specific user-defined type can be overloaded either as global operators or as member operators. In most cases, however, neither of these leads to satisfactory inheritance.

Global Operators and Friends

The global case is easy to take care of: not being a member of the base class, there is no possibility of inheritance. Note that operators declared as "friends" of the base class are still global, so these are not member operators. (It is still possible that tese may be re-used, taking advantage of automatic upcasting.) The friend modifier only gives them access to protected and private class members, presumably for reasons of implementation. Finally, friendship itself is not inherited: A friend of the base class has no implied rights to access protected or private derived class variables.

Member Operators

Before considering member operators and inheritance, it is helpful to analyze the implications of becoming a member opetaror. For any (non-static) class method, there is an implicit parameter tautomatically supplied to the method: the object that is making the call. This is immediately apparent for ordinary functions, because the dot notation has the calling object mentioned explicitly:

Box b;                       // declares variable of type Box
b.SetDimensions(10,20,30);   // sets dimensions of b to 10, 20, 30

Note that b is a parameter for the member function that is essential: it determines which box is to have its dimensions set.

For member operator-functions, however, it is not so apparent how this implicit parameter is supplied to the operator, so there is a rule: The first operator parameter is used as the implicit parameter for any member operator. (You can review the previous discussions of assignment and bracket operators to verify this rule was followed.) Combined with the static syntax rule for operator overloading (which states an operator may be overloaded, but its call syntax and number of parameters may not be changed), inheritance is often not as useful as one would hope. As a general rule, it is usually best practice to override operators explicitly in a derived class.

It is worthwhile exploring these issues with some specific examples: equality, input, and assignment operators for class Box.

I/O Operators

The prototype for the I/O operators for a type T are as follows:

std::ostream& operator << (std::ostream& os, const T& t); // outputs t to os
std::istream& operator >> (std::istream& is, T& t);       // inputs T object to t

Note that the first parameter is a reference to a stream object, and typically that reference is returned by the operator. Because the only type T parameter is the second parameter, these operators cannot be effectively overloaded as member functions.

Assignment Operator

This is one operator that cannot be overloaded as a global operator, by rule: the assignment operator must be a member operator. A typical overload of assignment for Box would look something like this:

Box& Box::operator= (const Box& b)
{
  if (this != b)
  {
    length = b.length;
    width = b.width;
    height = b.height;
  }
  return *this;
}

Now, because Cube derives from Box, Cube does in fact inherit the assignment operator. However, look at the prototype of what is inherited:

Box& operator= (const Box& b);

This prototype won't work for Cube objects, because there is no automatic downcast rule to go with the

Automatic Upcast Rule: A derived class object will automatically be considered a base class object when required by parameter typing.

The automatic upcast rule captures the essence of inheritance by allowing a derived class object to be considered a base class object when required, without explicit cast: it is often said that a derived class object is_a base class object. Thus the input parameter for the Box assignment operator would accept a Cube object by automatically upcasting it to a Box object. The problem arises in dealing with the return value: The newly constructed object is constructed as a Box and returned as such, but what would be expected is a Cube object, and because there is no automatic downcasting, there is a mismatch of return types.

So we are 0 for 2 in attempts at inheriting base class member operator implementatons.

Equality and Order Operators

The equality and order operators can be overloaded as either global operators or as class member operators. Here are the prototypes as global operators for a type T:

int operator == (const T& t1, const T& t2);
int operator != (const T& t1, const T& t2);
int operator <  (const T& t1, const T& t2);
int operator <= (const T& t1, const T& t2);
int operator >  (const T& t1, const T& t2);
int operator => (const T& t1, const T& t2);

and here are the prototypes as members of the class T:

int T::operator == (const T& t2) const;
int T::operator != (const T& t2) const;
int T::operator <  (const T& t2) const;
int T::operator <= (const T& t2) const;
int T::operator >  (const T& t2) const;
int T::operator => (const T& t2) const;

Note that the parameter t1 mentioned in the global versions is missing in the member versions - it has been supplanted by the implicit first parameter that is the object making the call. There is a certain lack of symmetry in the member versions, but that is an observation appealing only to a sense of aesthetics. For the client program, the operator syntax is identical for either choice. The dirfferences are apparent only when the operator function syntax is used:

// client code fragments
T t1, t2;     // declare two variables of type T
...
if (t1 == t2) // operator syntax
{
  ...
}

t1.operator==(t2); // operator function syntax, member option
operator==(t1,t2); // operator function syntax, global option

Now consider the meanings of the expression c1 == c2 in the following client code fragment:

Cube c1, c2;
...
if (c1 == c2)  // what does this mean? 
{
  ...
}

If the operator is declared global for class Box, then the two Cube objects c1 and c2 will be upcast to Box objects and compared. If the operator is declared as a member of class Box, a similar metamorphasis occurs: c1 has inherited the operator from its parent class Box, so it calls the member operator with parameter c2. Then c2 is upcast, in essence comparing the Cube c1 to the Box c2.

It is probably OK to re-use this operator in either option. However, there are times when it could lead to results that were NOT expected: when the derived class has added variables to those inherited from the base class, these variables will not be considered in the comparison, because the upcast type does not "know" about these variables. The best advice is: don't rely on inheritance of operator implementations or on upcasting to re-use global operator overloads.

Alternatives to Inheritance

Inheritance is a powerful mechanism, especially in applications, with many desirable attributes:

Especially when used in a dynamic application with runtime binding of objects, as discussed in the next chapter, it is hard to find better application/modeling environments. There are other circumstances, particularly where runtime binding of objects is not needed, where alternatives to OOP may be worth considering.

For example, suppose that we have a class Truck that inherits from a class Vehicle. We also want Truck to have a cargo area modelled as a box. We could use inheritance to make a truck "be" a box, or we could let a truck "have" a box. These are the two points of view:

The following declares and implements a rudimentary Truck class by inheriting a box:

class TruckA : public Vehicle, public Box
{
public:
  TruckA (float length, float width, float height)  : Vehicle(), Box(length, width, height)
  {}
};

In this option, TruckA is a Box (as well as being a Vehicle). In the second option, defined below, TruckB has a Box as a variable named cargo_box.

class TruckB : public Vehicle
{
public:
  TruckB (float length, float width, float height)  : Vehicle(), cargo_box(length, width, height)
  {}
  Box cargo_box;
};

Note that the Box constructor is called in the initialization list for the Truck constructor in either case: (A) as a call to the base class constructor, and (B) as an initialization for the member variable cargo_box.

The "is_a / has_a" debate has been around for a while, and probably should be encouraged to stay. There will not be one correct answer for all situations, and it is good to be aware of the alternatives.