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:
- Compile-time polymorphism: Achieved through function overloading and operator overloading, also known as static polymorphism. It enables methods or operators to behave differently based on the parameters or the types they are working with. Compile-time polymorphism occurs when using function overloading and operator overloading.
- Runtime polymorphism: Achieved through inheritance and virtual functions, also known as dynamic polymorphism. Runtime polymorphism is typically achieved through inheritance and virtual functions, which allow derived classes to override methods defined in a base class.
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:
IPrintableandISavableare abstract classes acting as interfaces.Documentimplements bothIPrintableandISavable, fulfilling the contract of each interface by defining theprint()andsave()methods.
§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.
Interfaces define a contract without specifying how it must be implemented. This allows different classes to implement the same interface in diverse ways. For example, an
AudioPlayerinterface might be implemented differently by anMP3Playerand aWAVPlayer, each providing its unique functionality while still fulfilling theAudioPlayercontract.By coding to interfaces rather than specific implementations, you minimize dependencies between different parts of your code. If one part of the code expects an object that implements an interface, it doesn’t need to know about the specific class providing the functionality. For instance, a function that works with a
Drawableinterface doesn’t need to know if it’s handling aCircle,Rectangle, orTriangle, only that it can calldraw().Interfaces enable polymorphism, allowing objects of different classes to be treated uniformly if they implement the same interface. This makes it easier to substitute one implementation for another. For instance, if a
Loggerinterface is used throughout a codebase, you can swap aConsoleLoggerwith aFileLoggerwithout altering any of the code that depends onLogger.Interfaces encourage modular code design by keeping implementations separate from the functionalities they provide. This separation promotes reusability, as the same interface can be implemented by multiple classes. Additionally, as requirements evolve, new implementations of an interface can be added without changing existing code.
Using interfaces can improve testability. When dependencies are defined by interfaces, you can easily substitute mock or stub implementations for testing purposes. For example, a
DatabaseInterfacecould have a mock implementation for unit testing, avoiding the need to interact with a real database.
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:
performTaskworks with anyLogger, whether it’s aConsoleLoggerorFileLogger, promoting loose coupling.- New logging mechanisms, like
NetworkLogger, can be introduced without changing theperformTaskfunction.
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 #
- What is the difference between compile-time polymorphism and runtime polymorphism?
- How does compile-time polymorphism differ from runtime polymorphism in terms of performance?
- 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.
- How does using interfaces promote loose coupling?
- Describe a scenario where multiple interfaces would be useful in a C++ program.
- What keyword must be added to a base class member function to enable runtime polymorphism?
- What keyword should be used in a derived class to explicitly indicate that a function is overriding a base class virtual function?
- What is a pure virtual function and how is it declared?
- 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?
- Why does the pointer in this code call
Child::greet()rather thanBase::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; }
- What can go wrong if a base class destructor is not declared
virtual?
- Write a C++ abstract interface class named
IResizablethat enforces two methods:void resize(float factor)andfloat getSize() const. Include all necessary keywords.
- Can you instantiate an abstract class directly?