Lab 16: Polymorphism

Polymorphism

In the last few labs, we introduced polymorphism briefly, but we haven’t yet fully explained the concept. Polymorphism is a fundamental concept in C++ object-oriented programming that allows objects of different classes to be treated as instances of a common base class. It enables a single action to be performed in various ways, typically through the use of inheritance and pointers or references to base classes. Polymorphism is a useful tool for making code more flexible and for simplifying complex systems by reducing redundancy and centralizing shared behavior.

§1 Polymorphism in C++ #

Polymorphism allows you to call derived class functions through a base class pointer or reference, enabling different behaviors depending on the object type. There are two primary types of polymorphism:

Example of Runtime Polymorphism:

class Animal {
public:
  virtual void speak() const {
    std::cout << "Animal speaks" << std::endl;
  }
  virtual ~Animal() = default;
};

class Dog : public Animal {
public:
  void speak() const override {
    std::cout << "Woof!" << std::endl;
  }
};

class Cat : public Animal {
public:
  void speak() const override {
    std::cout << "Meow!" << std::endl;
  }
};

int main() {
  Animal* animal1 = new Dog();
  Animal* animal2 = new Cat();

  animal1->speak();  // Outputs: Woof!
  animal2->speak();  // Outputs: Meow!
}

In this example, the speak function is called based on the actual type of the object (Dog or Cat), not the type of pointer (Animal), thanks to polymorphism.

§2 Refresher: Multiple Inheritance #

Multiple inheritance occurs when a class inherits from more than one base class. This is often used when a derived class needs functionalities from multiple classes.

Example: Multiple Public Inheritance

class Flyable {
public:
  void fly() const {
    std::cout << "I can fly!" << std::endl;
  }
};

class Swimmable {
public:
  void swim() const {
    std::cout << "I can swim!" << std::endl;
  }
};

// The syntax for multiple inheritance uses a comma between each "parent" class
class Duck : public Flyable, public Swimmable {
};

int main() {
  Duck d;
  d.fly();   // Outputs: I can fly!
  d.swim();  // Outputs: I can swim!
}

In this example, Duck inherits from both Flyable and Swimmable, gaining both fly() and swim() capabilities.

§3 Naming Conflicts in Multiple Inheritance #

Multiple inheritance can be powerful, but it introduces a risk of naming conflicts when two or more base classes contain members (functions or variables) with the same name. To resolve this, you must specify the base class you want to access.

Example: Resolving Naming Conflicts

class ClassA {
public:
  void display() const {
    std::cout << "Display from ClassA" << std::endl;
  }
};

class ClassB {
public:
  void display() const {
    std::cout << "Display from ClassB" << std::endl;
  }
};

// Inherits both ClassA and ClassB, with both implementing display()
class Derived : public ClassA, public ClassB {
public:
  void displayBoth() const {
    ClassA::display();  // Specify ClassA version of display()
    ClassB::display();  // Specify ClassB version of display()
  }
};

int main() {
  Derived d;
  d.displayBoth();
}

Here, Derived inherits a display() method from both ClassA and ClassB. To resolve this conflict, we use the ClassA::display() and ClassB::display() syntax to specify which display() function to call.

§4 Multiple Interfaces #

In C++, interfaces are typically represented by abstract classes with pure virtual functions. Thanks to C++’s support for multiple inheritance, a class can implement multiple interfaces, indicating that it provides several distinct functionalities.

Example: Implementing Multiple Interfaces

class IPrintable {
public:
  virtual void print() const = 0;  // Pure virtual function
};

class ISavable {
public:
  virtual void save() const = 0;   // Pure virtual function
};

// Document class implements both the IPrintable and ISavable interfaces
class Document : public IPrintable, public ISavable {
public:
  void print() const override {
    std::cout << "Printing Document" << std::endl;
  }

  void save() const override {
    std::cout << "Saving Document" << std::endl;
  }
};

int main() {
  Document doc;
  doc.print();  // Outputs: Printing Document
  doc.save();   // Outputs: Saving Document
}

In this example:

§5 Designing Flexible Code with Interfaces #

Interfaces can be used to structure your program in a way that allows objects to implement specific functionalities as needed. By using interfaces, you can achieve loose coupling between objects and other parts of your code, enabling flexible and modular design.

Example: Loose Coupling with Interfaces: Consider a logging system where you want to log messages to different outputs (e.g., console, file, or network). Using an interface for logging, you could design the system to log through any object implementing that interface:

class Logger {
public:
  virtual void log(const std::string& message) = 0;
  virtual ~Logger() = default;
};

class ConsoleLogger : public Logger {
public:
  void log(const std::string& message) override {
    std::cout << "Console: " << message << std::endl;
  }
};

class FileLogger : public Logger {
public:
  void log(const std::string& message) override {
    // Code to write to a file
  }
};

void performTask(Logger& logger) {
  // Code that performs a task and logs information
  logger.log("Task started.");
  // Task code...
  logger.log("Task completed.");
}

In this example:

Using interfaces effectively helps you create a more adaptable, maintainable, and testable codebase. By enabling loose coupling, interfaces reduce dependencies, allowing different parts of a program to interact without needing to know specifics about each other’s implementations. This approach supports scalable and modular code that can evolve over time with minimal refactoring.

§6 Questions #

  1. What is the difference between compile-time polymorphism and runtime polymorphism?
  1. How does compile-time polymorphism differ from runtime polymorphism in terms of performance?
  1. When using multiple inheritance, how do you resolve a name conflict, such as both parent classes containing a method named print()? provide a simple code example.
  1. How does using interfaces promote loose coupling?
  1. Describe a scenario where multiple interfaces would be useful in a C++ program.
  1. What keyword must be added to a base class member function to enable runtime polymorphism?
  1. What keyword should be used in a derived class to explicitly indicate that a function is overriding a base class virtual function?
  1. What is a pure virtual function and how is it declared?
  1. What is the name given to a class that contains a pure virtual function? What effect does a pure virtual function have on the class?
  1. Why does the pointer in this code call Child::greet() rather than Base::greet()?
    class Base {
    public:
      virtual void greet() const { std::cout << "Hello from Base" << std::endl; }
    };
    class Child : public Base {
    public:
      void greet() const override { std::cout << "Hello from Child" << std::endl; }
    };
    int main() {
      Base* ptr = new Child();
      ptr->greet();
      delete ptr;
    }
    
  1. What can go wrong if a base class destructor is not declared virtual?
  1. Write a C++ abstract interface class named IResizable that enforces two methods: void resize(float factor) and float getSize() const. Include all necessary keywords.
  1. Can you instantiate an abstract class directly?