Inheritance

Inheritance

Overview

We have a fair amount to cover about Inheritance in C++

  • Base and Derived Classes
  • Access Control and Derivation
    • Member Access from Derived Classes
    • protected Class Members
    • Access Declarations
  • Inheritance and Constructors  
  • Inheritance and Destructors
  • Inherited Member Functions
  • Non-inherited Member Functions
  • Upcasting and Downcasting

So, let’s get on to those topics…

Base and Derived Classes

A class can inherit the features or functionality of another class through class derivation.

  • The class that inherits is called the derived class
  • The class that is inherited from is called the base class.

A class can be derived to :

  • Restrict or reinterpret the base class.
  • Factor out common functionality for reuse.
  • Specialize or augment the base class to add a new layer of functionality

The syntax is:

class Base 
{   
	// Base class members 
};

class Derived : public Base // inherits from Base
{  
	// Derived class members 
};

Example

This declares two classes: Student and GradStudent. GradStudent is a class derived from class Student

//
//  Student.hpp
//  Base and Derived Classes
//
//  Created by Bryan Higgs on 8/24/24.
//

#ifndef Student_hpp
#define Student_hpp

#include <string>

// Student.h
class Student
{
public:
  Student(const int  id,
          const std::string &name);
  void Print() const;
  double GetGPA() const;
  void SetGPA(double gpa);
  // ...
private:
   int         m_id;  // Student ID
   std::string m_name; // Student name
   double      m_gpa; // Grade Point Average
};

class GradStudent : public Student
{
public:
  GradStudent(const int id,
              const std::string &name,
              const std::string &dept);
  void Print() const;
  const std::string &GetThesisTitle() const;
  void SetThesisTitle(std::string &thesis_title);
  // ...
private:
   std::string  m_dept;         // Department
   std::string  m_thesis_title; // Thesis title
};

#endif /* Student_hpp */

A class derived from class X is a kind of  (or is a) X.

A class containing a member of class Y has a Y.

For example, classes Vehicle, Airplane, and Car. Vehicles have a Color, A Car has an Engine, and four Wheels.

//
//  Vehicle.hpp
//  Base and Derived Classes
//
//  Created by Bryan Higgs on 8/25/24.
//

#ifndef Vehicle_h
#define Vehicle_h

enum class Color{White, Black, Green, Blue};

class Vehicle
{
  // ...
private:
  double max_speed;
  int    max_occupants;
  Color  color;
};

class Airplane : public Vehicle
{
  // An Airplane is a Vehicle
  // ...
private:
  double m_wingspan;
};

enum class Engine(4Cylindar, 8Cylinder, Rotary)

class Car : public Vehicle
{
  // A Car is a Vehicle
  // ...
  // A Car has an Engine, wheels
private:
  Engine engine;
  Wheel  wheel[4];
};

#endif /* Vehicle_h */

An instance of a derived class can be used anywhere an instance of its base class can be used:

  • The derived class is a superset of the base class and so contains all the members of the base class.

The reverse is not true;  a base class cannot be used in place of a class derived from it:

  • It does not have all the members of the derived class.

Base & Derived Class Conversions

For example, conversion can take place in one direction only:

//
//  Vehicle.cpp
//  Base and Derived Classes
//
//  Created by Bryan Higgs on 8/25/24.
//

#include <stdio.h>

#include "Vehicle.hpp"

void DoConversions()
{
  Vehicle base;
  Car     derived;
  
  base = derived;
  derived = base;
}

This code, given the previous class declarations for Vehicle and Car, produces the following:

base = derived compiles correctly.

Note that when a derived class is converted to a base class, the result has lost its derived class members — it has been ‘sliced’.

but:

derived = base produces a compile-time error:

Vehicle.cpp:18:11 No viable overloaded '='

Overriding Class Members

A derived class can define a member with the same name as a base class  member.

This is called overriding (or refinement).

You can override a data member, or a member function:

//
//  Base.hpp
//  Base and Derived Classes
//
//  Created by Bryan Higgs on 8/25/24.
//

#ifndef Base_h
#define Base_h

class Base
{
public:
  Base(int i = 0) : m_i(i)  {}
  void Print() const;
private:
  int m_i;
};

class Derived : public Base
{
public:
  Derived(int x, int i) : Base(x), m_i(i) {}
  void Print() const;
private:
  int m_i;
};

#endif /* Base_h */
//
//  Base.cpp
//  Base and Derived Classes
//
//  Created by Bryan Higgs on 8/25/24.
//

#include <iostream>
#include "Base.hpp"

void Base::Print() const
{
  std::cout << m_i << std::endl;  // Base m_i
}

void Derived::Print() const
{
  std::cout << m_i << std::endl;  // Derived m_i
}
//
//  main.cpp
//  Base and Derived Classes
//
//  Created by Bryan Higgs on 8/24/24.
//

#include <iostream>
#include "Base.hpp"

int main(int argc, const char * argv[]) 
{
  Base b(37);
  Derived d(52, 101);

  b.Print();
  d.Print();
  
  return 0;
}

Produces the following output:

37
101
Program ended with exit code: 0

Can you explain this output?

Class Vector

Remember our Vector class?

//
//  Vector.h
//  Base & Derived Classes - Vector
//
//  Created by Bryan Higgs on 8/26/24.
//

#ifndef Vector_h
#define Vector_h

class Vector // A vector of ints
{
public:
  // Constructors & destructors
  explicit Vector(const int elements = 10);
  Vector(const Vector &source);
  Vector(const int source[],
         const int elements);
  ~Vector() { delete [] m_p; }

  int getElementCount() { return m_elems; }
  
  // Subscript operator overload
  int &operator[](const int i);
  
private:
  int *m_p;    // Ptr to array
  int  m_elems;  // Alloc. count
};

#endif /* Vector_h */
//
//  Vector.cpp
//  Base & Derived Classes - Vector
//
//  Created by Bryan Higgs on 8/26/24.
//

#include <iostream>
#include "Vector.h"

Vector::Vector(const int elements)
{
  // Allocate space for elements
  m_p = new int[m_elems = elements];
  // Set all elements to 0
  for (int i = 0; i < m_elems; i++)
    m_p[i] = 0;
}

Vector::Vector(const Vector &source)
{
  // Allocate space for elements
  m_p = new int[m_elems = source.m_elems];
  // Copy values from source elements
  for (int i = 0; i < m_elems; i++)
    m_p[i] = source.m_p[i];
}

Vector::Vector(const int source[],
               const int elements)
{
  // Allocate space for elements
  m_p = new int[m_elems = elements];
  // Copy values from source elements
  // (assuming that source is properly sized)
  for (int i = 0; i < m_elems; i++)
    m_p[i] = source[i];
}

// Subscript operator overload
int &Vector::operator[](const int i)
{
  // Do bounds checking...
  if ( (i < 0) || (i >= m_elems) )
  {
    std::cerr << "Illegal Vector index: "
              << i << " (max: "
              << (m_elems - 1) << ')'
              << std::endl;
    exit(1);  // Abrupt exit!
  }
  // We're within bounds
  return m_p[i];
}

Class BoundedVector

Let’s create a class BoundedVector that is, in effect, a more generalized form of the Vector class: when an instance is created, the vector bounds are specified. While a Vector instance is created with a specified number of elements, and those elements may be accessed using the subscript operator, with subscripts from 0 to (<number-of-elements> – 1), a BoundedVector is created with a low bound and a high bound, and a size calculated from the low and high bounds.

The BoundedVector class is derived from the Vector class, and relies on its base class for some of its features.

//
//  BoundedVector.h
//  Base & Derived Classes - Vector
//
//  Created by Bryan Higgs on 8/26/24.
//

#ifndef BoundedVector_h
#define BoundedVector_h

#include "Vector.h"

class BoundedVector : public Vector
{
public:
  BoundedVector(int low, int high);
  int LowBound() { return m_low; }
  int HighBound() { return m_high; }
  int &operator[](int i)
  { return Vector::operator[](i-m_low); }
private:
  int    m_low;  // Low bound
  int    m_high;   // High bound
};

#endif /* BoundedVector_h */
//
//  BoundedVector.cpp
//  Base & Derived Classes - Vector
//
//  Created by Bryan Higgs on 8/26/24.
//

#include <iostream>
#include "BoundedVector.h"

// Constructor
BoundedVector::BoundedVector(int low, int high)
  : Vector((high - low) + 1),  // NOTE!
    m_low(low), m_high(high)
{
  if (low > high)      // Check the bounds
  {
    std::cerr << "\nLow bound " << low
              << " greater than high bound"
              << high << std::endl;
    exit(0);      // Abrupt exit!
  }
}

Here’s a simple test program for these classes:

//
//  main.cpp
//  Base & Derived Classes - Vector
//
//  Created by Bryan Higgs on 8/26/24.
//

#include <iostream>
#include "BoundedVector.h"

int main(int argc, const char * argv[]) 
{
  Vector         values(6);
  BoundedVector  bvalues(-5, 3);

  for (int i = 0; i < values.getElementCount(); i++)
  {
    values[i] = i;
  }
  std::cout << "Vector contents: " << std::endl;
  for (int i = 0; i < values.getElementCount(); i++)
  {
    std::cout << "[" << i << "] = "
              << values[i] << std::endl;
  }

  for (int i = bvalues.LowBound(); i <= bvalues.HighBound(); i++)
  {
    bvalues[i] = i*i;
  }
  std::cout << "BoundedVector contents: " << std::endl;
  for (int i = bvalues.LowBound(); i <= bvalues.HighBound(); i++)
  {
    std::cout << "[" << i << "] = "
              << bvalues[i] << std::endl;
  }

  return 0;
}

Which outputs the following:

Vector contents: 
[0] = 0
[1] = 1
[2] = 2
[3] = 3
[4] = 4
[5] = 5
BoundedVector contents: 
[-5] = 25
[-4] = 16
[-3] = 9
[-2] = 4
[-1] = 1
[0] = 0
[1] = 1
[2] = 4
[3] = 9
Program ended with exit code: 0

Member Access Control from Derived Classes

Remember that a class can use member access control to specify access to members from outside the class:

class Bumble 
{    
	int   a;  // private 
public:    
	float b;  // public 
private:    
	long  c;  // private    
	// ... 
}; 

An access specifier can be applied to a base class reference:

class Mumble : public Bumble 
{ 
private:    
	int   x; 
public:    
	float y;    
	long  z;    
	// ... 
};

which controls what access the derived class will have to the base class members.

The default access specifier for a base class is:

  • private when the derived class is a class 
  • public when the derived class is a struct

If we derive a class using a public base class (public inheritance):

class Derived : public Base { ... };

then the public members of the base class become publicly accessible through the derived class.

If we derive a class using a private base class (private inheritance):

class Derived : private Base { ... };

then the public members of the base class become privately accessible from within the derived class.

Regardless of the base class access specified, private members of a base class are not accessible from within the derived class or through it (unless access is explicitly granted through friend declarations — something to use very sparingly).

It’s analogous to a class being a member of another class:

class B { ... };
class C { ... };
class A
{
public:
	B	my_b;	// public members of B 
			// accessible through class A
private:
	C	my_c;	// public members of C not 
			// accessible through class A
};

When you inherit from a base class using public inheritance, you have an ‘is-a’ (or ‘is-a-kind-of’) relationship:

class Car : public Vehicle { ... };

A Car ‘is-a[-kind-of]’ Vehicle.

But beware!

When you inherit from a base class not using public inheritance, you do not have an ‘is-a’ relationship!

Unfortunately, the default:

class Car : Vehicle { ... };

is equivalent to private inheritance:

class Car : private Vehicle { ... };

which is not what you normally want!

For example:

private:    
	int a; 
public:    
	int b; 	// Parent1's member functions have 
			// access to:  a, b 
};			// Normal code has access, through
			// class Parent1, to: b

class Child1 : private Parent1 { 
private:    
	int c; 
public:    
	int d; 	// Child1's member functions have 
			// access to:  b, c, d 
};			// Normal code has access, through 
			// class Child1, to: d

class Child2 : public Parent1 { 
private:    
	int e; 
public:    
	int f; 	// Child2's member functions have 
			// access to:  b, e, f 
};			// Normal code has access, through 
			// class Child2, to:  b, f

protected Class Members

It quickly became apparent that the simple private vs public access control was too ‘all or nothing’ to handle inheritance appropriately. The relationship between a derived class and its base class(es) is often closer than that of a class with the rest of the world.

To allow access from derived classes to base class members, it often became necessary to make the base class members public. However, this exposes those members to the rest of the world.  Not good!

So, there came into being a third access specifier, protected, which provides a middle ground between private and public:

class Bumble
{
private:	// accessible only within class
	int a;
protected:	// ...class or derived classes
	float b;
public:	    // ...anyone
	long c;
	// ...
};

which can also be applied to a base class (protected inheritance):

class Mather : protected Blather { ... };

protected only affects access to base class members from derived classes. 

Don’t bother using protected in a class if the class will never be used as a base class.  A class needs to be designed to act as a base class.

Here’s a summary:

Base Class Member Access Specifierpublic Inheritanceprotected Inheritanceprivate Inheritance
publicpublic in derived classprotected in derived classprivate in derived class
protectedprotected in derived classprotected in derived classprivate in derived class
privatenot accessible in derived classnot accessible in derived classnot accessible in derived class
Summary of base class member access from a derived class

For example:

//
//  Hierarchy.h
//  Protected Class Members
//
//  Created by Bryan Higgs on 8/26/24.
//

#ifndef Hierarchy_h
#define Hierarchy_h

class A
{
private:
  int a;
protected:
  int b;
public:
  int c;
};

class B : public A
{
private:
  int d;
public:
  void Test();
};

class C : private A
{
private:
  int e;
public:
  void Test();
};

#endif /* Hierarchy_h */
//
//  Hierarchy.cpp
//  Protected Class Members
//
//  Created by Bryan Higgs on 8/26/24.
//

#include "Hierarchy.h"

void B::Test()
{
  a = 1;  // ERROR -- no access
  b = 2;
  c = 3;
  d = 4;
}

void C::Test()
{
  a = 1;  // ERROR -- no access
  b = 2;
  c = 3;
  e = 4;
}

Compile-time errors:

Hierarchy.cpp:12:3 'a' is a private member of 'A'

Hierarchy.cpp:20:3 'a' is a private member of 'A'
//
//  main.cpp
//  Protected Class Members
//
//  Created by Bryan Higgs on 8/26/24.
//

#include "Hierarchy.h"

int main(int argc, const char * argv[]) 
{
  B  myB;
  C  myC;
  myB.a = 1;  // ERROR -- no access
  myB.b = 2;  // ERROR -- no access
  myB.c = 3;
  myB.d = 4;  // ERROR -- no access
  myC.a = 1;  // ERROR -- no access
  myC.b = 2;  // ERROR -- no access
  myC.c = 3;  // ERROR -- no access
  myC.e = 4;  // ERROR -- no access

  return 0;
}

Compile-time errors:

main.cpp:14:7 'a' is a private member of 'A'
main.cpp:15:7 'b' is a protected member of 'A'
main.cpp:17:7 'd' is a private member of 'B'
main.cpp:18:7 'a' is a private member of 'A'
main.cpp:19:7 'b' is a private member of 'A'
main.cpp:20:7 'c' is a private member of 'A'
main.cpp:21:7 'e' is a private member of 'C'

public, private, and protected Inheritance

public inheritance is used when the inheritance is part of the interface.

  • The fact that X is-a Y is something you are willing to tell your clients about.

private inheritance is used when the inheritance is not part of the interface

  • It is an implementation detail.
  • The relationship is more like reuses-a.
  • Rarely used — using composition (member class object) usually works as well, and often is simpler.

protected inheritance is used when the inheritance is part of the interface to the derived classes, but is not part of the interface to the clients.

  • Even more rarely used — using composition is usually as good and simpler.

Inheritance with Constructors and Destructors

Constructors

Constructors are not inherited

A derived class constructor can (and by default will) call base class constructors

Constructors cannot be virtual (see later)

A public default constructor and a  public copy constructor are generated by the compiler where they are needed.

When an instance of a derived class is created:

  • If the constructor for the derived class does not explicitly call a base class constructor in its initializer list, then the default base class constructor is automatically called.  
  • If there is no default constructor for the base class, then a compile-time error occurs.
  • If a base class constructor is explicitly called in the derived class initializer list, then that is the base class constructor executed; there is no default base class constructor initialization.

The order of constructor execution is:

  • Base class constructor(s), in declaration order (independent of initializer list order)
  • Derived class member constructor(s), in declaration order (independent of initializer list order) 
  • The body of the derived class constructor.

Virtual base classes are a special case (see later).

Destructors

A destructor cannot be inherited.

A derived class destructor can (and by default will) call its base class destructors.

A destructor can be virtual (see later).

  • In fact, a base class destructor should be virtual.

A public default destructor is generated  by the compiler when no destructor is explicitly declared. It calls the destructors for base classes and members of the derived class.

The order of destructor execution is:

  • The body of the derived class destructor.
  • Derived class member destructor(s), in reverse declaration order
  • Base class destructor(s), in reverse declaration order

In other words, the exact opposite of constructor execution order.

Virtual base classes are a special case (see later).

Example:

//
//  Hierarchy.h
//  Constructors and Destructors
//
//  Created by Bryan Higgs on 8/26/24.
//

#ifndef Hierarchy_h
#define Hierarchy_h

class A
{
public:
  A();
  ~A();
};

class B
{
public:
  B();
  ~B();
};

class C : public A // C is derived from A
{
public:
  C();
  ~C();
private:
  B b; // Member class object
};

class D : public C // D is derived from C
{
public:
  D();
  ~D();
private:
  A a; // Member class object
};

#endif /* Hierarchy_h */
//
//  Hierarchy.cpp
//  Constructors and Destructors
//
//  Created by Bryan Higgs on 8/26/24.
//

#include <iostream>
#include "Hierarchy.h"

// class A
A::A()
{
  std::cout << "in A constructor" << std::endl;
}

A::~A()
{
  std::cout << "in A destructor" << std::endl;
}

// class B
B::B()
{
  std::cout << "in B constructor" << std::endl;
}

B::~B()
{
  std::cout << "in B destructor" << std::endl;
}

// class C
C::C()
{
  std::cout << "in C constructor" << std::endl;
}

C::~C()
{
  std::cout << "in C destructor" << std::endl;
}

// class D
D::D()
{
  std::cout << "in D constructor" << std::endl;
}

D::~D()
{
  std::cout << "in D destructor" << std::endl;
}
//
//  main.cpp
//  Constructors and Destructors
//
//  Created by Bryan Higgs on 8/26/24.
//

#include "Hierarchy.h"

int main(int argc, const char * argv[]) 
{
  D d;

  return 0;
}

Produces the following output:

in A constructor
in B constructor
in C constructor
in A constructor
in D constructor
in D destructor
in A destructor
in C destructor
in B destructor
in A destructor
Program ended with exit code: 0

Can you explain it?

Safe & Unsafe Downcasting

You can suppress compiler errors resulting from implicit downcasting, by explicitly downcasting, but it’s usually not a good idea:

//
//  Header.h
//  Safe Downcasting
//
//  Created by Bryan Higgs on 8/26/24.
//

#ifndef Header_h
#define Header_h

class Base
{
public:
  Base() { a = 10; }
private:
  int a;
};

class Derived : public Base
{
public:
  Derived() { myValue = 44; }
  double GetMyValue() { return myValue; }
private:
  double myValue;
};

#endif /* Header_h */
//
//  main.cpp
//  Safe Downcasting
//
//  Created by Bryan Higgs on 8/26/24.
//

#include <iostream>
#include "Header.h"

void process(Base &b)
{
  Derived d;
  d = (Derived &)b;    // OK, no compile time error
  std::cout << "Derived value = " << d.GetMyValue() << std::endl;
}

int main(int argc, const char * argv[]) 
{
  Base b;
  Derived d;
  b = d;        // OK, but sliced
  process(d);   // No problems
  process(b);   // Serious problems!  Why?

  return 0;
}

Here’s what the above program produces:

Derived value = 44
Derived value = 6.95161e-310
Program ended with exit code: 0

See the problem?

Can you explain it?

What is the problem?

Explicit casts from a base class (pointer or reference) are inherently unsafe:

  • You never know what the actual class instance really is
  • If it’s not the type you’re casting it to, then bad things will happen!
  • If it hurts, stop doing it!

But there are situations where you really need to do this. (Although I would think hard about whether you really want to do it!)

How can we do this, and retain safety?

We would like some kind of mechanism where, if the cast isn’t valid, a run-time error will occur.

dynamic_cast

The ISO C++ committee invented the dynamic_cast for such situations:

Shape *s = ...
Rectangle *r = dynamic_cast<Rectangle *>(s);
if (r != 0)
		// The object pointed to by s is a
		// Rectangle, and r points to it
else
		// The object pointed to by s is not a
		// Rectangle, and r is a null pointer

In general, the form of a dynamic_cast is:

dynamic_cast<T>(v)

where T is a type (subject to some restrictions), and v is an expression.

Note that dynamic_cast is not limited to downcasting.

The type T must be:

  • A pointer to a complete class type, in which case:
    • v must be an rvalue of a pointer to a complete class type
    • the result is an rvalue of type T
    • if the cast fails, the result is a null pointer;  you have to test for it!
    • This case is like a test in your program.
  • or a reference to a complete class type, in which case:
    • v must be an lvalue of a complete class type
    • the result is an lvalue of the type referred to by T
    • if the cast fails, a bad_cast exception occurs;  you have to catch it!
    • This case is like an assertion;  if it fails, then you get an exception. (More about exceptions, later…)
  • or a “pointer to cv void“. (“cv” here is a shorthand for “possibly cv-qualified”, which means “may have a const or a volatile on it”) — Now, we’re really getting down into the weeds. Let’s ignore this for now…

Summary

Well, we’ve learned quite a bit about inheritance:

  • Base and Derived classes
  • Access Control and Derivation
    • Member Access from Derived classes
    • protected class members
    • Access declarations
  • Inheritance and Constructors
  • Inheritance and Destructors
  • Inherited Member Functions
  • Non-inherited Member Functions
  • Upcasting and Downcasting

All of the above discussion was about single inheritance.  We’ll see later the considerable complexities introduced by multiple inheritance

Onward!

Index