C++ Multiple Inheritance: Comprehensive Guide
Concept & Appropriate Use Cases
Multiple inheritance allows a class to derive from multiple base classes simultaneously. This is particularly useful for:
- Implementing interfaces (pure abstract classes)
- Combining orthogonal functionalities
- Mixin-based development
- Simulating traits or aspects
A classic example is a 'FlyingCar' inheriting from both 'Car' and 'Aircraft'.
#include <iostream>
#include <string>
#include <utility>
// Interface classes
class Printable {
public:
virtual void print() const = 0;
virtual ~Printable() = default;
};
class Serializable {
public:
virtual std::string toJSON() const = 0;
virtual ~Serializable() = default;
};
// Concrete implementation
class Report : public Printable, public Serializable {
std::string content;
public:
explicit Report(std::string c) : content(std::move(c)) {}
void print() const override {
std::cout << "Report: " << content << '\n';
}
std::string toJSON() const override {
return "{\"report\": \"" + content + "\"}";
}
};
The Diamond Problem & Virtual Inheritance
When two parent classes inherit from the same grandparent, a derived class may contain duplicate grandparent subobjects (the diamond). Virtual inheritance ensures there is only one shared subobject of the virtual base.
Consider this hierarchy:
Person
/ \
Employee Student
\ /
TeachingAssistant
With virtual inheritance, the most-derived class is responsible for constructing the virtual base.
#include <string>
#include <utility>
class Person {
std::string name;
public:
explicit Person(std::string n) : name(std::move(n)) {}
};
// Virtual inheritance
class Employee : virtual public Person {
std::string department;
public:
Employee(std::string n, std::string d)
: Person(std::move(n)), department(std::move(d)) {}
};
class Student : virtual public Person {
std::string major;
public:
Student(std::string n, std::string m)
: Person(std::move(n)), major(std::move(m)) {}
};
class TeachingAssistant : public Employee, public Student {
public:
TeachingAssistant(std::string n, std::string d, std::string m)
: Person(n), // virtual base constructed here (most-derived)
Employee(n, d),
Student(n, m) {}
};
Method Resolution Order
In multiple inheritance, name lookup considers all base classes. If a non-virtual member with the same name exists in multiple bases, an unqualified call is ambiguous and must be disambiguated.
Key points:
1. Base class subobjects are initialized left-to-right as listed in the inheritance list.
2. For non-virtual members with the same name in different bases, use qualified calls (e.g., A::f(), B::f()).
3. For virtual functions, dynamic dispatch calls the most-derived override; ambiguity arises only when the compiler cannot pick a single candidate at compile time.
#include <iostream>
class A { public: void f() { std::cout << "A"; } };
class B { public: void f() { std::cout << "B"; } };
class C : public A, public B {};
int main() {
C c;
// c.f(); // Error: ambiguous
c.A::f(); // Outputs A
std::cout << '\n';
c.B::f(); // Outputs B
}
Practical Patterns
- Interface Segregation: Inherit from multiple abstract interfaces
- Mixin Pattern: Small classes adding specific behaviors
- Policy-Based Design: Template parameters as compile-time alternatives
- Decorator Pattern: Prefer composition for layering behavior
- CRTP: Static polymorphism without virtual functions
Best Practices Summary
Do | Don't |
---|---|
Use for interface implementation | Use for implementation inheritance across many concrete bases |
Keep inheritance lists short (<3) | Create complex diamond hierarchies unnecessarily |
Make interfaces pure abstract | Combine unrelated responsibilities in one type |
Document the design and construction rules | Assume relationships are self-evident |
Consider composition first | Default to multiple inheritance by habit |