Lab 15: Multiple Inheritance

Multiple Inheritance

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:

§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();
}

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:

§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.

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;
};

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 #

  1. Provide the declaration line for a class named Duck which inherits from two classes named Bird and Swimmer
  1. What causes the Diamond Problem in multiple inheritance?
  1. How can you resolve the Diamond Problem?
  1. Explain the difference between “is-a” and “has-a” relationships.
  1. Provide an example of an “is-a” relationship.
  1. Provide an example of an “has-a” relationship.
  1. What effect does the friend keyword have?
  1. Why should friend functions be used sparingly?
  1. Why is a virtual destructor important in an interface?
  1. Why should you avoid using virtual inheritance all the time?
  1. When designing a program which is preferable in most cases: composition or inheritance?
  1. Provide a code example of an interface class for Rotatable objects, Name the interface class appropriately and include all necessary keywords. The interface class will enforce the methods:

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.

  1. You are designing a software model for electronic devices. There are different types of devices like Computer’s and Smartphone’s. Both have storage but differ significantly in functionality. Should storage be represented as a base class, inherited by Computer and Smartphone, or should it be a component within each device type? Explain your reasoning.
  1. In a library system, a Book has various properties like a title, author, and ISBN number. You also need to manage details like Publisher information and Edition. Would it make more sense to model Publisher and Edition as separate classes used within Book (composition), or to make Book inherit from Publisher or Edition? Justify your choice.
  1. In a university management system, you have C++ classes for Employee and Teacher. A Teacher is an Employee but also performs unique tasks such as teaching and mentoring students. Should Teacher inherit from Employee, or should Employee include Teacher as a data member? Explain why.
  1. In a project management tool, each Employee can work on multiple Project’s, and each Project involves multiple Employee’s. Should you design Employee and Project classes with one inheriting from the other, or should each class contain references to the other? Justify your answer.
  1. You are designing a simple chess game, each GamePiece such as Rook, Knight, and Queen are placed on the game board for players to interact with. Should pieces like Rook, Knight, and Queen inherit from GamePiece, or should they be components of GamePiece? Explain your reasoning.