Multiple inheritance happens if more than one base class is declared in the base specifier list of a derived class. In that case, the derived class will contain a subobject for each of those bases and will inherit their members. The technique is powerful but leads to questions about construction order, name clashes, and, most famously, the diamond problem.
The solution in the language for the diamond problem is virtual inheritance.
Virtual inheritance is a way of ensuring that a shared base class will appear in the final object exactly once, regardless of how many paths to it exist.
A simple example of multiple inheritance. First of all, we create two completely independent base classes.
#include <iostream>
// Define the members that belong to this type
class Printer
{
public:
void print(const char* msg) const
{
std::cout << "Printer: " << msg << '\n';
}
};
class Scanner
{
public:
void scan() const
{
std::cout << "Scanner: scanning document\n";
}
};
class MultifunctionDevice : public Printer, public Scanner
{
};
int main()
{
MultifunctionDevice device;
device.print("hello");
device.scan();
return 0;
}
The MultifunctionDevice class is publicly derived from two independent classes, Printer and Scanner. An object of MultifunctionDevice has, therefore, two subobjects: one Printer subobject and one Scanner subobject. When a program calls device.print("hello"), the C++ implementation will pick the version from Printer, because device.scan() invokes the function from Scanner.
The object is constructed in left to right order of the base classes as they are declared: first the Printer object, then Scanner, and last the MultifunctionDevice object itself.
Destroying the object is performed in reverse order.
However, the situation becomes complicated if both classes provide a member function (or member data) with the same name. Consider this version of the base classes:
#include <iostream>
class A
{
public:
void identify() const
{
std::cout << "I am A\n";
}
};
class B
{
public:
void identify() const
{
std::cout << "I am B\n";
}
};
class C : public A, public B
{
};
int main()
{
C obj;
// obj.identify(); // error: request for member 'identify' is ambiguous
obj.A::identify();
obj.B::identify();
return 0;
}
As soon as both A and B provide a member function named identify, the program suffers from name collisions. The compiler can no longer determine automatically which identify function to call on an unqualified call obj.identify(). A program with the code above is therefore ill formed, as far as name lookup is concerned.
To remove the ambiguity, we can qualify the call: calling obj.A::identify() clearly picks up the function declared in A, while obj.B::identify() invokes the other one.
This works for data members and nested types as well.
The most complex problem of multiple inheritance, the so called diamond problem, occurs when a class is derived from two intermediate classes that themselves are derived from a common base class. Typically, this diagram represents the class hierarchy.
#include <iostream>
class PowerDevice
{
public:
PowerDevice()
{
std::cout << "PowerDevice constructed\n";
}
void power(int watts) const
{
std::cout << "Powering with " << watts << " watts\n";
}
};
class Printer : public PowerDevice
{
public:
Printer()
{
std::cout << "Printer constructed\n";
}
};
class Scanner : public PowerDevice
{
public:
Scanner()
{
std::cout << "Scanner constructed\n";
}
};
class MultifunctionDevice : public Printer, public Scanner
{
public:
MultifunctionDevice()
{
std::cout << "MultifunctionDevice constructed\n";
}
};
int main()
{
MultifunctionDevice device;
// device.power(100); // error: ambiguous
device.Printer::power(100);
return 0;
}
The object device will contain two PowerDevice subobjects, the one that is accessed via the Printer object, and another one that is accessed via the Scanner. If a program were to call device.power(100), the call would be ambiguous because there are two PowerDevice subobjects accessible. Worse, this would actually result in the PowerDevice constructor and destructor being called twice for each object, which is almost never what one wants for an instance of a shared power source or supply.
Virtual inheritance gets rid of the duplication: a base class marked as virtual will appear at most once in the most derived object of a class, irrespective of how many distinct paths lead to it.
#include <iostream>
class PowerDevice
{
public:
PowerDevice()
{
std::cout << "PowerDevice constructed\n";
}
void power(int watts) const
{
std::cout << "Powering with " << watts << " watts\n";
}
};
// Enable dynamic dispatch through this function
class Printer : virtual public PowerDevice
{
public:
Printer()
{
std::cout << "Printer constructed\n";
}
};
class Scanner : virtual public PowerDevice
{
public:
Scanner()
{
std::cout << "Scanner constructed\n";
}
};
class MultifunctionDevice : public Printer, public Scanner
{
public:
MultifunctionDevice()
{
std::cout << "MultifunctionDevice constructed\n";
}
};
int main()
{
MultifunctionDevice device;
device.power(100);
return 0;
}
Both Printer and Scanner now virtually derive from PowerDevice. A MultifunctionDevice object will therefore contain only a single PowerDevice subobject, ensuring that the call device.power(100) is unambiguous, and that PowerDevice is constructed (and destroyed) exactly once. The construction order will change in comparison to the non virtual approach: the most derived class takes care of constructing the virtual bases first, before constructing its non virtual bases. As you will see, the messages will come in the order "PowerDevice constructed", "Printer constructed", "Scanner constructed", "MultifunctionDevice constructed", and the destruction order will be reversed.
Since there is only one copy of the virtual base object, only the most derived class is allowed to invoke a non default constructor of it. If PowerDevice was designed to be constructed with a particular parameter, the constructor code must belong in the most derived class, or come out of the member initializer list of the most derived class:
#include <iostream>
class PowerDevice
{
// Members below this label are private
private:
int m_watts{};
public:
explicit PowerDevice(int watts)
: m_watts{ watts }
{
std::cout << "PowerDevice constructed with "
<< m_watts << " watts\n";
}
};
class Printer : virtual public PowerDevice
{
public:
Printer()
: PowerDevice{ 0 }
{
}
};
class Scanner : virtual public PowerDevice
{
public:
Scanner()
: PowerDevice{ 0 }
{
}
};
class MultifunctionDevice : public Printer, public Scanner
{
public:
MultifunctionDevice()
: PowerDevice{ 150 }
{
std::cout << "MultifunctionDevice constructed\n";
}
};
The initializer PowerDevice{ 150 } would thus only be allowed in the member initializer list of MultifunctionDevice itself. While the intermediate classes (Printer, Scanner) would also get the opportunity to list PowerDevice as a base, those particular initializer slots would simply be ignored at construction time for a derived object.
In conclusion, multiple inheritance enables you to merge several independent sets of functionality or data into a new class. If these sets of functionality share a common foundation, virtual inheritance ensures that this foundation will not be duplicated, but exist just once in the final combined class, under the control of the most derived class.
The price you pay for this is some extra complexity when describing and building objects of the most derived classes. If the design truly needs this level of component combination, then the combination of multiple and virtual inheritance will be the solution you require, while for other cases, standard single inheritance remains the appropriate and much simpler choice.
Exercise: Multiple and Virtual InheritanceStarter code, hints and solutionOpenClose
Practice: Multiple and Virtual Inheritance
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Bind Printer& and Scanner& views to device and invoke their operations. Identify the static view types and complete object type, and explain whether virtual dispatch is involved.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
// Define the members that belong to this type
class Printer
{
public:
void print(const char* msg) const
{
std::cout << "Printer: " << msg << '\n';
}
};
class Scanner
{
public:
void scan() const
{
std::cout << "Scanner: scanning document\n";
}
};
class MultifunctionDevice : public Printer, public Scanner
{
};
int main()
{
MultifunctionDevice device;
device.print("hello");
device.scan();
return 0;
}Hints
MultifunctionDevice inherits from both Printer and Scanner. device.print and device.scan find members in different base classes.
This starter is simple multiple inheritance, not a shared-base diamond. For each call, identify the supplying base; do not assume virtual inheritance is involved merely because there are two bases.
Worked solution
The solution explicitly binds Printer& and Scanner& views to the same MultifunctionDevice, then calls through those views. These functions are nonvirtual, so their selection comes from the reference types; the complete object is still a MultifunctionDevice.
#include <iostream>
// Define the members that belong to this type
class Printer
{
public:
void print(const char* msg) const
{
std::cout << "Printer: " << msg << '\n';
}
};
class Scanner
{
public:
void scan() const
{
std::cout << "Scanner: scanning document\n";
}
};
class MultifunctionDevice : public Printer, public Scanner
{
};
int main()
{
MultifunctionDevice device;
device.print("hello");
device.scan();
Printer& printer = device;
Scanner& scanner = device;
printer.print("via Printer reference");
scanner.scan();
return 0;
}
Example output
Printer: hello
Scanner: scanning document
Printer: via Printer reference
Scanner: scanning document
0 comments
No comments yet. Be the first.