Lab 14: Inheritance Continued

More Inheritance Features

In the previous section, we covered the fundamental concepts of inheritance in C++. In this session, we will expand further on more C++ inheritance features that enable useful code structures for working with complex data types.

§1 Upcasting and Downcasting #

Upcasting converts a derived reference or pointer to its base type implicitly. Downcasting converts a base class reference to a derived type. These conversions are useful for storing and working with different object types within the same list or data structures.

Assume we have a base class Animal and a pair of derived classes: Dog and Duck:

Upcasting: (moving “up” the inheritance tree) occurs when you cast a derived class to a base class. This is safe and automatic.

Dog myDog;
Animal* animalPtr = &myDog;  // Upcasting a Dog* pointer to Animal*

Downcasting: (moving “down” the inheritance tree) Occurs when converting a base class pointer to a derived class type. This requires an explicit cast and is less safe since it assumes the base class reference is actually pointing to a derived class object.

Types of Downcasting:

// downcast `animal` pointer to derived class type
Dog* dogPtr = static_cast<Dog*>(animal);
dogPtr->bark();
// downcast `animal` pointer to derived class type
if (Dog* dogPtr = dynamic_cast<Dog*>(animal)) {
  // able to call derived class methods here
  dogPtr->bark();
}

§2 Identifying Class Pointer Types #

you can use dynamic_cast to discover the real derived type behind a base pointer. This is another technique that allows you to store different object types together, and later be able to identify and utilize the objects true type.

class Animal {
public:
  void eat() { cout << "eating food." << endl; };
  // Make Animal polymorphic by declaring the default destructor `virtual`
  // more on this in the next section.
  virtual ~Animal() = default;
};

class Duck : public Animal {
public:
  void quack() { cout << "Quack."; }
};

class Dog : public Animal {
public:
  void bark() { cout << "Bark."; }
};

int main() {
  Dog dog;
  Animal animal;
  Duck duck;
  // create an array of Animal pointers, referencing the derived objects
  Animal *animalArr[3] = {&dog, &animal, &duck};
  // use dynamic casts to identify pointer type and execute code based on type
  for (Animal *animal : animalArr) {
    if (Dog* dogPtr = dynamic_cast<Dog*>(animal)) {
      dogPtr->bark();
      dogPtr->eat();
    }
    else if (Duck *duckPtr = dynamic_cast<Duck *>(animal)) {
      duckPtr->quack();
      duckPtr->eat();
    }
    else {
      cout << "Not a Dog or Duck.";
      animal->eat();
    }
  }
}

Outputs:

$ ./example
Bark. eating food.
Not a Dog or Duck. eating food.
Quack. eating food.

In this example, we use dynamic_cast to cast a base pointer to its appropriate derived class, this allows us to identify which type a base pointer represents and adapt our program behavior. notice how the base class methods eat() are always available and valid in all cases.

This can be useful when storing lists of different objects, as we can use polymorphism to change behavior based on the object type.

§3 Abstract Base Class (Pure Virtual Functions) #

An abstract base class serves as a blueprint for other classes. It defines a common interface (typically using virtual functions) for derived classes, but it does not provide a complete implementation. This enables polymorphism, allowing you to handle objects of different derived classes uniformly.

For a class to be abstract, it must contain at least one pure virtual function. This is a virtual function that has no implementation in the base class. It’s declared by setting the value of the function equal to zero = 0. When a base class has a pure virtual function, it becomes abstract, and no objects of that class can be instantiated. You must declare a derived class to utilize any methods in an abstract base class.

class Shape {
public:
  virtual void draw() = 0;  // Pure virtual function, the `Shape` class is now abstract
};

In the example above, Shape is an abstract class because it contains a pure virtual function draw(). Any derived class of Shape must implement the draw() function, or it too will become abstract. Abstract classes cannot be instantiated.

for example:

class Shape {
public:
  virtual void draw() = 0;  // Pure virtual function

  // Regular member function with implementation
  void info() const {
    cout << "This is a shape." << endl;
  }
};

class Circle : public Shape {
public:
  void draw() const override {  // implements abstract `draw()` function
    cout << "Drawing a Circle." << endl;
  }
};

int main() {
  Circle circle;
  Shape* shape = &circle; // abstract class pointers can still reference derived objects
  shape->draw();  // automatically calls the `Circle` implementation of draw() 
  shape->info();
}
$ ./example
Drawing a Circle.
This is a shape.

§4 Direct and Indirect Base Classes #

In C++, when a class inherits from another class, that base class becomes part of the inheritance hierarchy. If this base class itself inherits from another class, we refer to the original as an indirect base class.

Example:

class Animal {  // Base class: Animal
public:
  void speak() { cout << "Animal Sound\n"; }
};

class Mammal : public Animal {  // Derived from Animal
public:
  void run() { cout << "Mammal Running\n"; }
};

// Derived from Mammal (indirectly inherits Animal through Mammal)
class Dog : public Mammal {
public:
  void bark() { cout << "Woof!\n"; }
};

int main() {
  Dog dog;
  dog.speak();  // Inherited from Animal
  dog.run();    // Inherited from Mammal
  dog.bark();   // Defined in Dog
}
$ ./example
Animal Sound
Mammal Running
Woof!

The inheritance structure of this example can be visualized with this UML diagram:

Indirect Inheritance allows Dog to access Animal’s member functions, even though Dog does not directly inherit from Animal.

§5 Further Reading #

§6 Questions #

  1. What is upcasting in C++ inheritance and why is it safe?
  1. When casting a base class pointer to a derived class, is static_cast or dynamic_cast more performant?
  1. What is the main downside of using static_cast?
  1. Why is dynamic casting useful when using an array of base class pointers, such as in Animal* animalArr[3]?
  1. What causes a class to become an abstract class?
  1. Can you create an instance of an abstract class?
  1. Explain why this code does not work, what change is needed to get the main() function to run as-is?
class Shape {
public:
  // Pure virtual function
  virtual void draw() const = 0;

  // Virtual destructor (important for base classes with virtual functions)
  virtual ~Shape() = default;
};

class Circle : public Shape {
private:
  int size = 0;
public:
  void create(int size) {this->size = size;}
  void draw() {}
};

int main() {
  Circle* circle = new Circle();
}
  1. Is an abstract base class allowed to implement methods?
  1. What is indirect inheritance?

Don’t forget to submit your UML for the associated programming assignment!