This lab covers advanced C++ concepts surrounding multiple inheritance and composition. This lab aims to clarify best practices and potential pitfalls when designing class hierarchies in C++.
§1 Multiple Inheritance #
C++ allows a class to inherit from more than one base class directly. This feature is powerful but can lead to complex scenarios, particularly with data members or functions inherited from multiple base classes that share the same names.
Let’s understand multiple inheritance with a visual example:
class Bird { // Base class: Bird
public:
void fly() { cout << "Flying\n"; }
};
class Swimmer { // Another base class: Swimmer
public:
void swim() { cout << "Swimming\n"; }
};
// Derived class from Bird AND Swimmer
class Duck : public Bird, public Swimmer {
public:
void quack() { cout << "Quack!\n"; }
};
int main() {
Duck duck;
duck.fly(); // From Bird
duck.swim(); // From Swimmer
duck.quack(); // From Duck
}
This can be visualized as:
The Duck class is able to inherit from multiple different base classes: Bird and Swimmer, inheriting the members from both base classes at once.
§2 The Diamond Problem #
The diamond problem occurs when a derived class inherits from multiple base classes that share a common base class, leading to ambiguity.
Example:
// Base class: Device
class Device {
public:
void identify() { cout << "Device\n"; }
};
class Computer : public Device {
public:
void compute() { cout << "Computing\n"; }
};
class Phone : public Device {
public:
void call() { cout << "Calling\n"; }
};
// Derived from both Computer and Phone
class Smartphone : public Computer, public Phone {};
int main() {
Smartphone smartphone;
// smartphone.identify(); // ERROR: Ambiguous
}
Visualized:
In this code:
Smartphoneinherits from bothComputerandPhone, each of which inherits fromDevice.- When an instance of
Smartphoneis created, implicit instances of the base classesComputerandPhoneare also created, which each implicitly use their own instance ofDevice. - Calling
identify()onsmartphoneleads to ambiguity, as the compiler cannot decide which base class’sidentify()to use (could use eitherComputer’s parentDeviceinstance orPhone’s parentDeviceinstance).
§3 Solving the Diamond Problem: Virtual Inheritance #
To resolve ambiguity, C++ provides virtual inheritance. When a class virtually inherits a base class, it ensures only one instance of the base class is present in the hierarchy, even when multiple paths exist.
// Base class: Device
class Device {
public:
void identify() { cout << "Device\n"; }
};
// Derived classes using virtual inheritance
class Computer : virtual public Device { //<-- declare virtual inheritance
public:
void compute() { cout << "Computing\n"; }
};
// both derived classes must declare the shared parent class as `virtual`
class Phone : virtual public Device { //<-- declare virtual inheritance
public:
void call() { cout << "Calling\n"; }
};
// Derived class from Computer and Phone: Smartphone
class Smartphone : public Computer, public Phone {};
int main() {
Smartphone smartphone;
smartphone.identify(); // No ambiguity
smartphone.compute();
smartphone.call();
}
The virtual keyword ensures that only one instance of the base class Device is created, This solves the diamond problem as only one instance of the base class methods will exist.
Virtual inheritance comes with a performance cost, due to extra indirection and pointers created to implement the feature, only use virtual inheritance when resolving the diamond problem, it is not needed in most cases.
§4 Composition vs Inheritance (“Is-A” vs. “Has-A” Relationship) #
Inheritance: The “Is-A” Relationship
Inheritance is a mechanism in C++ where one class (the derived class) inherits properties and behaviors (data members and member functions) from another class (the base class). This relationship describes an “is-a” relationship: if class B inherits from class A, we say that “B is an A.”
For example, if we have a base class Animal, then a Dog class could inherit from it because “a dog is an animal.”
Composition: The “Has-A” Relationship
Composition is a way to build complex types by combining objects of other types, establishing a “has-a” relationship rather than an “is-a” relationship. Instead of inheriting from another class, a class can contain an object of another class as a member.
For example, a Car class could have an Engine object because “a car has an engine.”
Here’s an example where a Person class “has a” Address.
class Address {
public:
Address(string city, string country) : city(city), country(country) {}
void display() const {
cout << "City: " << city << ", Country: " << country << endl;
}
private:
string city;
string country;
};
class Person {
public:
Person(string name, Address addr) : name(name), address(addr) {}
void displayInfo() const {
cout << "Name: " << name << endl;
address.display();
}
private:
string name;
Address address; // Composition: Person "has an" Address
};
int main() {
Address myAddress("New York", "USA");
Person person("Alice", myAddress);
person.displayInfo();
}
Personcontains anAddressobject, establishing a “has-a” relationship.- The
displayInfo()function inPersonuses thedisplay()function fromAddressto print the address details. - Unlike inheritance,
Addressdoes not define any behavior or properties ofPerson; it is simply a component ofPerson.
When designing programs, it’s generally better to favor composition over inheritance, as it provides greater flexibility. In contrast, inheritance creates a fixed hierarchy that can make future modifications or extensions challenging.
§5 Friend Functions #
A friend function is a function that is given special permission to access private and protected members of a class. While a friend is not a member function of the class, it can access class members as if it were, which is useful for cases where you need external functions to interact closely with class internals.
To declare a friend function, use the friend keyword inside the class where you want to grant access. This function can then access the class’s private and protected data directly.
Friend function syntax: To declare a friend function, simply declare the function prototype within the class definition, and precede it with the friend keyword.
class Base {
private:
int private_data;
public:
Base(int data) : private_data(data) {}
// Declare a friend function
friend void showPrivateData(Base& obj);
};
In this example, showPrivateData() is a friend function of the Base class. Any function with the same signature (void showPrivateData(Base& obj)) has full access to Base’s private data, even when it is not a member of the Base class.
Friend Function Example:
class User {
private:
string name;
int user_id;
public:
User(string n, int id) : name(n), user_id(id) {}
// Declare the friend function for cross-access
friend void friendshipInfo(User& userA, User& userB);
};
// friend function can access private and protected members of both classes
void friendshipInfo(User& userA, User& userB) {
cout << "Checking friendship status between " << userA.name
<< " (ID: " << userA.user_id << ") and "
<< userB.name << " (ID: " << userB.user_id << ")\n";
// Additional logic inspecting private data of both classes could go here
}
int main() {
User user1("Alice", 101);
User user2("Bob", 202);
friendshipInfo(user1, user2);
}
In this example, the User class declares a friend function with the signature void friendshipInfo(User& userA, User& userB), a function with the same signature is declared outside of the class, it has full access to the class members as if it was a class member. Here we use friend to be able to read private data across two User object instances.
Best Practices:
- Friend functions break encapsulation because they can access a class’s private data. Use them sparingly and only when necessary.
- Friend functions are not inherited. If you want a function to access private members of both the base and any derived classes, you need to declare it as a friend in each class separately.
- A friend function is not part of the class’s interface, so it’s typically defined outside the class.
§6 Interfaces #
In object-oriented programming, an interface is a way to specify methods a class must implement without defining how they work. C++ does not have a dedicated interface keyword like some other languages, but interfaces are typically implemented using abstract classes.
- Interfaces provide a “contract” for subclasses, ensuring they implement certain methods.
- Interfaces allow polymorphism, enabling you to use a common interface to interact with objects of different classes.
- Interfaces promote loose coupling, meaning we can easily swap or extend components that follow the same interface.
- Interfaces do not define functionality, they only define methods that other classes must implement.
Defining an Interface in C++:
// Interface for a Shape
// Interface names usually start with a capital `I` to denote this class as an interface.
class IShape {
public:
// Pure virtual functions
virtual double area() const = 0;
virtual std::string name() const = 0;
// Default virtual destructor for cleanup
virtual ~IShape() = default;
};
area()andname()are pure virtual functions, makingIShapean interface.- The destructor is virtual and has a default implementation. This is important when deleting objects through a base class pointer to prevent memory leaks.
Implementing the Interface in Derived Classes:
Classes that derive from an interface must provide implementations for all pure virtual functions.
// Derived class implementing IShape interface
class Circle : public IShape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// Implement area()
double area() const override {
return 3.14159 * radius * radius;
}
// Implement name()
std::string name() const override {
return "Circle";
}
};
// Another derived class implementing IShape
class Rectangle : public IShape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// Implement area()
double area() const override {
return width * height;
}
// Implement name()
std::string name() const override {
return "Rectangle";
}
};
Using the Interface:
With polymorphism, you can use a pointer or reference to IShape to handle any object implementing the interface. Because of the interface, you always know the objects will implement the defined methods, in this example: name() and area().
// function printShapeInfo() takes an interface reference as a parameter
// will accept any object which inherits (implements) the interface
void printShapeInfo(const IShape& shape) {
std::cout << "Shape: " << shape.name() << ", Area: " << shape.area() << std::endl;
}
int main() {
Circle circle(5.0);
Rectangle rectangle(4.0, 6.0);
printShapeInfo(circle); // both of these objects implement (inherit) IShape
printShapeInfo(rectangle); // so this function works with either object
}
§7 Questions #
- Provide the declaration line for a class named
Duckwhich inherits from two classes namedBirdandSwimmer
- What causes the Diamond Problem in multiple inheritance?
- How can you resolve the Diamond Problem?
- Explain the difference between “is-a” and “has-a” relationships.
- Provide an example of an “is-a” relationship.
- Provide an example of an “has-a” relationship.
- What effect does the
friendkeyword have?
- Why should
friendfunctions be used sparingly?
- Why is a virtual destructor important in an interface?
- Why should you avoid using virtual inheritance all the time?
- When designing a program which is preferable in most cases:
compositionorinheritance?
- Provide a code example of an interface class for
Rotatableobjects, Name the interface class appropriately and include all necessary keywords. The interface class will enforce the methods:
void rotate(float angle)float getRotation().
For the remaining questions: you will decide and justify if the following scenarios should be implemented via inheritance or components. You must explain your reasoning for full credit.
- For example: a
CircleandRectangleshould be inherited from aShapebase class, this is an “is-a” relationship becauseCircleandRectangleare bothShapes.
- You are designing a software model for electronic devices. There are different types of devices like
Computer’s andSmartphone’s. Both have storage but differ significantly in functionality. Shouldstoragebe represented as a base class, inherited byComputerandSmartphone, or should it be a component within each device type? Explain your reasoning.
- In a library system, a
Bookhas various properties like atitle,author, andISBNnumber. You also need to manage details likePublisherinformation andEdition. Would it make more sense to modelPublisherandEditionas separate classes used withinBook(composition), or to make Book inherit fromPublisherorEdition? Justify your choice.
- In a university management system, you have C++ classes for
EmployeeandTeacher. ATeacheris anEmployeebut also performs unique tasks such as teaching and mentoring students. ShouldTeacherinherit fromEmployee, or shouldEmployeeincludeTeacheras a data member? Explain why.
- In a project management tool, each
Employeecan work on multipleProject’s, and eachProjectinvolves multipleEmployee’s. Should you designEmployeeandProjectclasses with one inheriting from the other, or should each class contain references to the other? Justify your answer.
- You are designing a simple chess game, each
GamePiecesuch asRook,Knight, andQueenare placed on the game board for players to interact with. Should pieces likeRook,Knight, andQueeninherit fromGamePiece, or should they be components ofGamePiece? Explain your reasoning.